com.itametis.jsonconverter.pathstrategy.PathDeserializer.java Source code

Java tutorial

Introduction

Here is the source code for com.itametis.jsonconverter.pathstrategy.PathDeserializer.java

Source

/* This file is part of JsonLegacyKiller.
 *
 * JsonLegacyKiller is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * JsonLegacyKiller is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with JsonLegacyKiller.  If not, see <http://www.gnu.org/licenses/gpl.txt >.
 *
 * If you need to develop a closed-source software, please contact us
 * at 'social@itametis.com' to get a commercial version of JsonLegacyKiller,
 * with a proprietary license instead.
 */
package com.itametis.jsonconverter.pathstrategy;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.itametis.jsonconverter.classpathscan.JsonMappingPackage;
import com.itametis.jsonconverter.classpathscan.ScannedClass;
import com.itametis.jsonconverter.classpathscan.ScannedField;
import com.itametis.jsonconverter.exception.JsonException;
import com.itametis.jsonconverter.ignorestrategy.ReadIgnoreStrategy;
import com.itametis.jsonconverter.namingStrategy.ReadJsonNamingStrategy;
import com.itametis.jsonconverter.pathstrategy.flattener.Flattenizer;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// @formatter:off
/**
 * Improved Deserialisation to change the structure of the Json before converting it into an object.
 *
 * Gson converts a String into a Json Tree. Each leaf is a JsonElement.
 * A JsonElement (abstract type) can be :
 * - A jsonObject : An Object as represented in the java World. This class would be represented as a JsonObject.
 * - A jsonPrimitive : like a int, a double or a String.
 * - A jsonArray : contains a list of other json element.
 * - A jsonNull : well... a null.
 *
 * A json tree looks globally like that :
 *
 * jsonObject''''''''''''''''''''''''''''''''''''
 * '''|''''''''''''''''''''''''''''''''''''''''''
 * '''|-----------------|---------------|''''''''
 * jsonObject'''''''jsonObject''''''jsonPrimitive
 * '''|'''''''''''''''''|''''''''''''''''''''''''
 * '''|'''''''''''''''''|---------------|''''''''
 * JsonArray''''''jsonPrimitive''''jsonPrimitive'
 * '''|''''''''''''''''''''''''''''''''''''''''''
 * '''|-----------------|---------------|''''''''
 * JsonObject'''''''JsonObject'''''''JsonObject''
 *
 * Each child of a JsonElement is called a member.
 * A JsonArray has a list of elements.
 *
 * @author <a href="mailto:chloe.mahalin@itametis.com">Chlo MAHALIN - ITAMETIS</a>
 */
// @formatter:on
public class PathDeserializer implements JsonDeserializer<Object> {

    private final static Logger LOGGER = LoggerFactory.getLogger(PathDeserializer.class.getSimpleName());

    /**
     * We use Gson to do the conversion.
     */
    private final Gson gson;

    /**
     * The identified Objects that are flagged as misplaced.
     */
    private final HashMap<String, ScannedField> fieldsToMove = new HashMap<>();

    /**
     * The flattened Json tree. Map of json name -> the list of Json Element with this name.
     */
    private HashMap<String, JsonElementList> jsonTree = new HashMap<>();

    /**
     * The top element of the Json tree.
     */
    private JsonElementProxy topElement;

    /**
     * The Deserializer that change the structure of a Gson tree.
     */
    public PathDeserializer() {
        //Instanciate a Gson access to convert to modified tree.
        this.gson = new GsonBuilder().setFieldNamingStrategy(new ReadJsonNamingStrategy())
                .setExclusionStrategies(new ReadIgnoreStrategy()).create();
    }

    @Override
    public Object deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
            throws JsonParseException {
        if (this.isAJsonnableObject(typeOfT)) {

            //1. search what is flagged with path.
            this.searchForObjectsToMove(typeOfT);
            if (this.existsObjectToMove()) {

                //2. duplicate jsonTree with JsonName.
                this.flattenJsonTree(typeOfT, json);

                //3. move misplaced nodes
                this.moveMisplacedNodes();

                //4. rebuild tree
                this.rebuildTree(this.topElement);
            }
        } else {
            LOGGER.info("The class " + typeOfT.getTypeName()
                    + " is not flagged as Jsonnable with @Jsonnable annotation.");
        }

        this.fieldsToMove.clear();
        this.jsonTree.clear();
        this.topElement = null;
        // The new structure is sent to Gson, to be converted in an object.
        return this.gson.fromJson(json, typeOfT);
    }

    /**
     * Indicated that the class type is a jsonnable object.
     *
     * @param type the class type
     *
     * @return true is the class is annotated with @Jsonnable.
     */
    private boolean isAJsonnableObject(Type type) {
        return JsonMappingPackage.getInstance().getJsonnableContent().containsKey(type);
    }

    /**
     * Search in the class details, the fields that may need to be moved.
     * The class panel is defined when initializing the JsonConverter class through the constructor.
     *
     * @param type the class type.
     */
    private void searchForObjectsToMove(Type type) {
        if (this.isAJsonnableObject(type)) {
            ScannedClass jsonnable = JsonMappingPackage.getInstance().getJsonnableContent().get(type);

            for (ScannedField field : jsonnable.getjSonFields()) {
                if (field.needsToBeMoved()) {
                    LOGGER.debug("Found Field to move : {} of type {}", field.getName(),
                            field.getField().getType().getSimpleName());
                    this.fieldsToMove.put(field.getNameInJson(), field);
                }
                if (field.isCollectionOrMap()) {
                    this.searchForObjectsToMove(field.getParameters().get(0));
                } else {
                    this.searchForObjectsToMove(field.getField().getType());
                }
            }
        } else {
            LOGGER.debug("Found non jsonnable field {}", type.getTypeName());
        }
    }

    /**
     * Indicated that there is fields at the wrong place.
     *
     * @return true if there are identified fields at the wrong place.
     */
    private boolean existsObjectToMove() {
        return !this.fieldsToMove.isEmpty();
    }

    /**
     * Create a dictionary between the Json Element and its name.
     * Delete the links between the Json Elements.
     * The tree is then an HashMap and every element knows who is their children/parent.
     *
     * @param type    the class type.
     * @param element the JsonElement.
     */
    private void flattenJsonTree(Type type, JsonElement element) {
        if (this.isAJsonnableObject(type)) {

            //Element name :
            String jsonClassName = JsonMappingPackage.getInstance().getJsonnableContent().get(type).getjSonName();

            this.topElement = new JsonElementProxy(element, jsonClassName, null);

            Flattenizer flattenizer = new Flattenizer();
            LOGGER.debug("Flatten top element of the json tree '{}'.", jsonClassName);

            try {
                //Start a recursive flattening.
                this.jsonTree = flattenizer.flatten(this.topElement);
            } catch (JsonException ex) {
                throw new JsonParseException(ex);
            }
        } else {
            throw new JsonParseException("Element on top '" + type.getTypeName()
                    + "' is not flagged as jsonnable with annotation @Jsonnable."
                    + "It can not be analysed. Litteral conversion will be done.");
        }
    }

    /**
     * Relocate fields that must be placed somewhere else.
     *
     * @throws JsonParseException
     */
    private void moveMisplacedNodes() throws JsonParseException {
        for (Map.Entry<String, ScannedField> toMove : this.fieldsToMove.entrySet()) {
            for (JsonElementProxy proxyToMove : this.jsonTree.get(toMove.getKey())) {

                //1. We remove.
                this.removeField(proxyToMove, toMove);

                //2. We insert.
                this.insertField(proxyToMove, toMove);
            }
        }
    }

    /**
     * Remove a field from an indicated parent.
     *
     * @param toMove the combo jsonName - field information.
     */
    private void removeField(JsonElementProxy element, Map.Entry<String, ScannedField> toMove) {

        JsonElementList parents = getParentFromPath(toMove.getValue().getPathInJson(), element);
        for (JsonElementProxy parent : parents) {
            if (parent == null) {
                throw new JsonParseException(
                        "received null as a parent for element " + element.getJsonName() + " while deleting.");
            }
            try {
                parent.removeChild(element);
                LOGGER.info("Removed node {} from parent {} (jsonName)", element.getJsonName(),
                        parent.getJsonName());
            } catch (JsonException ex) {
                throw new JsonParseException(ex);
            }
        }
    }

    /**
     * Insert a field to an indicated Parent.
     *
     * @param toMove        the combo jsonName - field information.
     * @param elementToMove the element to insert.
     */
    private void insertField(JsonElementProxy element, Map.Entry<String, ScannedField> toMove) {
        JsonElementList parents = getParentFromPath(toMove.getValue().getPathInCode(), element);
        for (JsonElementProxy parent : parents) {
            if (parent == null) {
                throw new JsonParseException(
                        "Received null as a parent for element " + element.getJsonName() + " while inserting.");
            }
            parent.addChild(element);
            LOGGER.info("Inserted node {} in parent {} (jsonName)", element.getJsonName(), parent.getJsonName());
        }
    }

    /**
     * Get the parent associated with the element according to the indicated path. The path is relative to the
     * position of the element.
     *
     * @param paths          the path.
     * @param currentElement the current element.
     *
     * @return the parents corresponding to the path.
     */
    private JsonElementList getParentFromPath(String[] paths, JsonElementProxy currentElement) {

        JsonElementList parents = new JsonElementList();
        parents.add(currentElement);
        for (String path : paths) {
            LOGGER.debug("Searching parent '{}' from {}", path, currentElement.getJsonName());
            for (JsonElementProxy parent : parents) {
                if (parent == null) {
                    this.returnErrorNoParent(path, parent);
                }

                parents.remove(parent);

                if ("".equals(path) || ".".equals(path)) { // Current
                    parents.add(parent.getParent());
                } else if ("..".equals(path)) { //On top
                    if (parent.getParent() == null) {
                        this.returnErrorNoParent(path, parent);
                    }
                    parents.add(parent.getParent().getParent());
                } else if (parent.getChildren().containsKey(path)) { //In children
                    parents.addAll(parent.getChildren().get(path));
                } else { //does not exists.
                    this.returnErrorNoParent(path, parent);
                }

                LOGGER.info("Indicating parent : {} found for element {} with path {}",
                        (parent == null ? "null" : parent.getJsonName()), currentElement.getJsonName(), path);
            }
        }
        return parents;
    }

    /**
     * Return an error when looking for a parent.
     *
     * @param path   the path.
     * @param parent the parent.
     */
    private void returnErrorNoParent(String path, JsonElementProxy parent) {
        String availableList = ".., .";
        for (String name : parent.getChildren().keySet()) {
            availableList += ", " + name;
        }
        LOGGER.error("Searched child path '{}'. But available paths were : [{}]", path, availableList);
        throw new JsonParseException(
                "Searched child path '" + path + "'. But available paths were : [" + availableList + "]");
    }

    /**
     * Rebuild a new Gson tree from flattened tree.
     *
     * @param node the top element.
     */
    private void rebuildTree(JsonElementProxy node) {
        if (node.getElement().isJsonObject()) {
            JsonObject nodeObject = node.getElement().getAsJsonObject();

            //For each child :
            for (Map.Entry<String, JsonElementList> entry : node.getChildren().entrySet()) {
                for (JsonElementProxy child : entry.getValue()) {

                    //If not part of a collection, then it is a primitive, a null or an object.
                    if (!child.isPartOfACollection()) {
                        nodeObject.add(child.getJsonName(), child.getElement());
                    }
                    //If part of a collection, then it is an array.
                    else {
                        //We create the array if it does not exists.
                        if (nodeObject.get(child.getJsonName()) == null) {
                            nodeObject.add(child.getJsonName(), new JsonArray());
                        }
                        //And add the child to the array.
                        JsonArray array = nodeObject.get(child.getJsonName()).getAsJsonArray();
                        array.add(child.getElement());
                    }
                    //We build the tree from the child.
                    this.rebuildTree(child);
                }
            }
        }
    }

}