jeudi 11 juin 2015

Java et la réflexion [4/4]

Maintenant que vous maîtriser les annotations et la réflexion en java, voici le code source complet du convertissant Java -> Mongo/Mongo -> Java :
package mongodb.tools;


import mongodb.tools.annotation.Id;
import mongodb.tools.annotation.Ignore;
import mongodb.tools.annotation.Version;
import mongodb.tools.exception.VersionMismatchException;
import org.bson.Document;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;

import java.beans.Introspector;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.*;

/**
 * Created by emeric_martineau on 11/03/2015.
 */
public class MongoDbMapping {
    /**
     * Field name of version.
     */
    public static final String VERSION_FIELD = "<version>";

    /**
     * Map to map primitive class.
     */
    private static final Map<Class<?>, Class<?>> primitiveMap = new HashMap<Class<?>, Class<?>>();

    static {
        primitiveMap.put(Boolean.TYPE, Boolean.class);
        primitiveMap.put(Byte.TYPE, Byte.class);
        primitiveMap.put(Character.TYPE, Character.class);
        primitiveMap.put(Short.TYPE, Short.class);
        primitiveMap.put(Integer.TYPE, Integer.class);
        primitiveMap.put(Long.TYPE, Long.class);
        primitiveMap.put(Double.TYPE, Double.class);
        primitiveMap.put(Float.TYPE, Float.class);
        primitiveMap.put(Void.TYPE, Void.TYPE);
    }

    /**
     * Convert object to MondogDB object. Support direct object (int, String...) and Iterable. Not support array, Map...
     *
     * @see mongodb.tools.annotation.Ignore
     * @see mongodb.tools.annotation.Id
     *
     * @param obj object to convert
     *
     * @return mongodb object
     *
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    public static Document toMongo(final Object obj)
            throws InvocationTargetException, IllegalAccessException {
        Document result;

        if (obj == null) {
            result = null;
        } else {
            result = new Document();

            Class tClass = obj.getClass();
            Version versionAnnotation = (Version) tClass.getAnnotation(Version.class);

            if (versionAnnotation != null) {
                result.append(VERSION_FIELD, versionAnnotation.version());
            }

            Method[] methods = tClass.getMethods();

            String methodName;

            // Fielname
            String fieldName;
            // Data of field
            Object data;
            // Use if data is iterable
            List temporaryList;
            // Current object of list
            Object objList;

            for (int i = 0; i < methods.length; i++) {
                methodName = methods[i].getName();

                if (methodName.startsWith("get") && !"getClass".equals(methodName)
                        && !methods[i].isAnnotationPresent(Ignore.class)
                        && !methods[i].isAnnotationPresent(Id.class)) {
                    // Found getter.
                    fieldName = Introspector.decapitalize(methods[i].getName().substring(3));

                    if (methods[i].getParameterCount() == 0) {
                        data = methods[i].invoke(obj);

                        if (data instanceof Iterator) {
                            Iterator it = ((List) data).iterator();
                            temporaryList = new LinkedList();

                            while (it.hasNext()) {
                                objList = it.next();

                                temporaryList.add(toMongo(objList));
                            }
                        }

                        result.append(fieldName, data);
                    }
                }
            }
        }

        return result;
    }

    /**
     * Convert MongoDb object to POJO
     *
     * @param mongoObj mongodb object
     * @param clazz class to instanciate
     *
     * @return object
     *
     * @throws IllegalAccessException
     * @throws InstantiationException
     * @throws VersionMismatchException if object is strict and version not match
     */
    public static Object fromMongoDb(final Document mongoObj, final Class clazz)
            throws IllegalAccessException, InstantiationException, VersionMismatchException, InvocationTargetException, ClassNotFoundException {
        Object result;

        if (mongoObj == null) {
            result = null;
        } else {
            result = clazz.newInstance();
            Class tClass = result.getClass();
            Method[] methods = tClass.getMethods();

            /* Class annotation */
            Version versionAnnotation = (Version) tClass.getAnnotation(Version.class);

            String property;
            Object value;

            /* Setter method */
            Method setter;
            /* Parameter of setter */
            Class<?> parameter;
            Type[] type;

            for (Map.Entry<String, Object> setKey : mongoObj.entrySet()) {
                property = setKey.getKey();
                value = setKey.getValue();

                if (VERSION_FIELD.equals(property)) {
                    // Check version
                    if (versionAnnotation != null && versionAnnotation.strict()) {
                        int version = Integer.valueOf((String) value);

                        if (version != versionAnnotation.version()) {
                            throw new VersionMismatchException(String.format("Version %d found, version %d expected.",
                                    version, versionAnnotation.version()));
                        }
                    }
                } else {
                    setter = findSetter(methods, property);

                    if (setter != null) {
                        parameter = setter.getParameterTypes()[0];
                        type = setter.getGenericParameterTypes();

                        if (parameter.isPrimitive()) {
                            invokePrimitive(result, setter, parameter, value);
                        } else if (parameter.isEnum()) {
                            invokeEnum(result, setter, parameter, value);
                        } else if (parameter.isArray()) {
                            // TODO
                        } else if (parameter.getName().equals("java.lang.String")) {
                            setter.invoke(result, value);
                        } else if (parameter == List.class) {
                               invokeList(result, setter, type, value);
                        }

                        System.out.println(parameter);
                        // si c'est un talbeau
                        // si c'est une map
                    }
                }
            }
        }

        return result;
    }

    /**
     * Find method setter with only one parameter.
     *
     * @param methods methods list
     * @param fieldName fieldName
     *
     * @return method
     */
    private static Method findSetter(final Method[] methods, final String fieldName) {
        String methodName = "set".concat(Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1));
        System.out.println(fieldName);
        // Search method
        Method m = null;

        for (Method currentMethod : methods) {
            if (methodName.equals(currentMethod.getName())) {
                // Now check only one parameter
                if (currentMethod.getParameterCount() == 1) {
                    m = currentMethod;
                    break;
                }
            }
        }

        return m;
    }

    /**
     * Invoke primitive setter.
     *
     * @param obj object to call setter
     * @param m method to call
     * @param parameter parametter of setter
     * @param value value to setup
     *
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    private static void invokePrimitive(Object obj, Method m, Class<?> parameter, Object value)
            throws InvocationTargetException, IllegalAccessException {
        if (parameter == Boolean.TYPE) {
            m.invoke(obj, Boolean.valueOf((String) value));
        } else if (parameter == Byte.TYPE) {
            m.invoke(obj, Byte.valueOf((String) value));
        } else if (parameter == Character.TYPE) {
            m.invoke(obj, Character.valueOf((char) value));
        } else if (parameter == Short.TYPE) {
            m.invoke(obj, Short.valueOf((String) value));
        } else if (parameter == Integer.TYPE) {
            m.invoke(obj, Integer.valueOf((String) value));
        } else if (parameter == Long.TYPE) {
            m.invoke(obj, Long.valueOf((String) value));
        } else if (parameter == Double.TYPE) {
            m.invoke(obj, Double.valueOf((String) value));
        } else if (parameter == Float.TYPE) {
            m.invoke(obj, Float.valueOf((String) value));
        }
    }

    /**
     * Invoke enum setter.
     *
     * @param obj object to call setter
     * @param m method to call
     * @param parameter parametter of setter
     * @param value value to setup
     *
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    private static void invokeEnum(Object obj, Method m, Class<?> parameter, Object value)
            throws InvocationTargetException, IllegalAccessException {
        Enum enumValue = Enum.valueOf((Class<Enum>) parameter, (String) value);

        m.invoke(obj, enumValue);
    }

    /**
     * Invoke collection setter.
     *
     * @param obj object to call setter
     * @param m method to call
     * @param parameter parametter of setter
     * @param value value to setup
     *
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    private static void invokeList(Object obj, Method m, Type[] parameter, Object value)
            throws InvocationTargetException, IllegalAccessException, InstantiationException,
            VersionMismatchException, ClassNotFoundException {
        List srcList = (List) value;
        List destList = null;

        Class classOfList = getGenericClass(parameter);

        if (value != null) {
            destList = new ArrayList(srcList.size());

            for (Object item : srcList) {
                if (item == null) {
                    destList.add(null);
                }else if (item instanceof Document) {
                    fromMongoDb((Document) item, classOfList);
                } else {
                    destList.add(item);
                }
            }
        }

        m.invoke(obj, destList);
    }

    /**
     * Found type of generic
     *
     * @param parameter parameter of method
     *
     * @return class to be instanciate
     *
     * @throws ClassNotFoundException
     */
    private static Class getGenericClass(Type[] parameter) throws ClassNotFoundException {
        Type type = ((ParameterizedTypeImpl) parameter[0]).getActualTypeArguments()[0];

        return Class.forName(type.getTypeName());
    }
}

vendredi 5 juin 2015

Java et la réflexion [3/4]

Une partie intéressant de la reflexion est la lecture des annotations.

Qu'est-ce que les annotation ?

Les annotation sont des métadonnées (des données supplémentaires) ajoutée à une classe, un attribut, une méthode.

Il existe 3 types d'annotations :
  • SOURCE : ces annotations sont ignorées par le compilateur mais produisent par exemple un message à la compilation comme @Override, @SuppressWarnings,
  • CLASS : ajoute des métadonnées à la classe mais ne sont pas accessibles à l'exécution,
  • RUNTIME : c'est ce type d'annotation qu'on va utiliser.

Reprenons l'exemple de MongoDB. Nous allons créer un simple mapper entre des objets java et du mongo.

Pour cela nous allons créer 3 annotations.
  • @Id : qui permet d'indiquer que la méthode est le getter/setter de l'ID MongoDB,
  • @Ignore : qui indique de ne pas mapper l'attribut de la classe java,
  • @Version : qui indique la version de l'objet

mongodb.tools.annotation.Id :
package mongodb.tools.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * To indicate that setter is for MongoDb Id.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Id {
}

mongodb.tools.annotation.Ignore :
package mongodb.tools.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Ignore method.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Ignore {
}

mongodb.tools.annotation.Version :
package mongodb.tools.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * To indicate that setter is for MongoDb Id.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Version {
    /**
     * Version of object.
     *
     * @return
     */
    public int version();

    /**
     * If strict when unserialize. Raise error if not same version.
     * @return
     */
    public boolean strict() default false;
}

Imaginons la méthode toMongo() qui convertit un object java en object MondoDB.
public static Document toMongo(final Object obj)
        throws InvocationTargetException, IllegalAccessException {
    Document result;

    if (obj == null) {
        result = null;
    } else {
        result = new Document();

        Class tClass = obj.getClass();
        Version versionAnnotation = (Version) tClass.getAnnotation(Version.class);

        if (versionAnnotation != null) {
            result.append(VERSION_FIELD, versionAnnotation.version());
        }
            
...

Nous prenons donc la classe de l'objet et sur cette classe nous récupérons l'annotation de type Version.class.
Il est important de noter que les annotations ne sont pas héritable. C'est à dire que si la classe Toto à l'annotation Version et que la classe Titi hérite de la classe Toto, l'annotation n'est pas reporter sur la classe Toto.
Il faudra remonter dans la hiérarchie des héritages pour prendre toutes les annotations.

Pour les méthodes, c'est à peu près identique :
...

Method[] methods = tClass.getMethods();

for (int i = 0; i < methods.length; i++) {
                methodName = methods[i].getName();

    if (methodName.startsWith("get") && !"getClass".equals(methodName)
            && !methods[i].isAnnotationPresent(Ignore.class)
            && !methods[i].isAnnotationPresent(Id.class)) {
...

Voilà, ce n'est pas plus difficile que ça.