org.dd4t.databind.builder.json.JsonModelConverter.java Source code

Java tutorial

Introduction

Here is the source code for org.dd4t.databind.builder.json.JsonModelConverter.java

Source

/*
 * Copyright (c) 2015 Radagio
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.dd4t.databind.builder.json;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.lang3.StringUtils;
import org.dd4t.contentmodel.Component;
import org.dd4t.contentmodel.Embedded;
import org.dd4t.contentmodel.FieldSet;
import org.dd4t.contentmodel.FieldType;
import org.dd4t.core.databind.BaseViewModel;
import org.dd4t.core.databind.ModelConverter;
import org.dd4t.core.databind.TridionViewModel;
import org.dd4t.core.exceptions.ItemNotFoundException;
import org.dd4t.core.exceptions.SerializationException;
import org.dd4t.databind.annotations.ViewModel;
import org.dd4t.databind.builder.AbstractModelConverter;
import org.dd4t.databind.util.DataBindConstants;
import org.dd4t.databind.util.JsonUtils;
import org.dd4t.databind.util.TypeUtils;
import org.dd4t.databind.viewmodel.base.ModelFieldMapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Resource;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import static org.dd4t.databind.util.DataBindConstants.ID;
import static org.dd4t.databind.util.DataBindConstants.LINKED_COMPONENT_VALUES_NODE;
import static org.dd4t.databind.util.DataBindConstants.MULTIMEDIA;
import static org.dd4t.databind.util.DataBindConstants.URL;
import static org.dd4t.databind.util.DataBindConstants.VALUES_NODE;

/**
 * JsonModelConverter.
 *
 * @author R. Kempees
 * @since 19/11/14.
 */
public class JsonModelConverter extends AbstractModelConverter implements ModelConverter {
    private static final Logger LOG = LoggerFactory.getLogger(JsonModelConverter.class);

    private Class<? extends org.dd4t.contentmodel.Field> concreteFieldImpl;

    @Resource(name = "dataBinder")
    protected JsonDataBinder databinder;

    @Override
    public <T extends BaseViewModel> T convertSource(final Object data, final T model)
            throws SerializationException {

        if (!JsonUtils.isValidJsonNode(data)) {
            LOG.debug("No data or not a JsonNode - nothing to do.");
            return null;
        }

        JsonNode rawJsonData = (JsonNode) data;

        LOG.debug("Conversion start.");
        this.concreteFieldImpl = databinder.getConcreteFieldImpl();
        if (model instanceof TridionViewModel) {
            LOG.debug("We have a Tridion view model. Setting additional properties");
            setTridionProperties((TridionViewModel) model, rawJsonData);
        }

        boolean isRootComponent = true;

        JsonNode contentFields = null;
        JsonNode metadataFields = null;
        Component.ComponentType componentType = Component.ComponentType.UNKNOWN;
        if (rawJsonData.has(DataBindConstants.COMPONENT_TYPE)) {
            componentType = Component.ComponentType
                    .findByValue(rawJsonData.get(DataBindConstants.COMPONENT_TYPE).intValue());
        }

        if (componentType == Component.ComponentType.NORMAL) {
            if (rawJsonData.has(DataBindConstants.COMPONENT_FIELDS)) {
                contentFields = rawJsonData.get(DataBindConstants.COMPONENT_FIELDS);
            }
        } else if (componentType == Component.ComponentType.MULTIMEDIA
                && rawJsonData.has(DataBindConstants.MULTIMEDIA)) {
            contentFields = rawJsonData.get(DataBindConstants.MULTIMEDIA);

        }

        if (rawJsonData.has(DataBindConstants.METADATA_FIELDS)) {
            metadataFields = rawJsonData.get(DataBindConstants.METADATA_FIELDS);
        }

        if (contentFields == null && metadataFields == null) {
            isRootComponent = false;
        }

        buildModelProperties(model, rawJsonData, isRootComponent, contentFields, metadataFields, componentType);
        return model;
    }

    private <T extends BaseViewModel> void buildModelProperties(final T model, final JsonNode rawJsonData,
            final boolean isRootComponent, final JsonNode contentFields, final JsonNode metadataFields,
            final Component.ComponentType componentType) throws SerializationException {
        final Map<String, Object> modelProperties = model.getModelProperties();

        try {
            for (Map.Entry<String, Object> entry : modelProperties.entrySet()) {

                final String fieldName = entry.getKey();
                LOG.debug("Key:{}", fieldName);

                ModelFieldMapping m = (ModelFieldMapping) entry.getValue();

                if (m.getViewModelProperty().isComponentLinkUrl() && isRootComponent) {
                    processComponentUrlField(model, rawJsonData, m);

                } else {

                    processFieldMapping(model, rawJsonData, isRootComponent, contentFields, metadataFields,
                            componentType, fieldName, m);
                }
            }
        } catch (IllegalAccessException | IOException e) {
            LOG.error("Error setting field!", e);
        }
    }

    private <T extends BaseViewModel> void processComponentUrlField(final T model, final JsonNode rawJsonData,
            final ModelFieldMapping m) throws SerializationException, IllegalAccessException {
        final Field urlField = m.getField();
        urlField.setAccessible(true);
        String componentId = rawJsonData.get(ID).textValue();
        String resolved = "";

        try {
            resolved = getLinkResolver().resolve(componentId);
        } catch (ItemNotFoundException e) {
            LOG.error("Could not resolve a link to: " + componentId, e);
        }

        urlField.set(model, resolved);
    }

    private <T extends BaseViewModel> void processFieldMapping(final T model, final JsonNode rawJsonData,
            final boolean isRootComponent, final JsonNode contentFields, final JsonNode metadataFields,
            final Component.ComponentType componentType, final String fieldName, final ModelFieldMapping m)
            throws IllegalAccessException, SerializationException, IOException {
        String fieldKey;
        fieldKey = getFieldKeyForModelProperty(fieldName, m);

        boolean isEmbedabble = false;
        if ((!rawJsonData.has(DataBindConstants.FIELD_TYPE_KEY) && !isRootComponent)
                || (rawJsonData.has(DataBindConstants.FIELD_TYPE_KEY) && (FieldType.findByValue(
                        rawJsonData.get(DataBindConstants.FIELD_TYPE_KEY).intValue()) == FieldType.EMBEDDED))

        ) {
            isEmbedabble = true;
        }

        if (componentType == Component.ComponentType.NORMAL || componentType == Component.ComponentType.UNKNOWN
                || m.getViewModelProperty().isMetadata()) {
            final JsonNode currentNode = getJsonNodeToParse(fieldKey, rawJsonData, isRootComponent, isEmbedabble,
                    contentFields, metadataFields, m);
            // Since we are now now going from modelproperty > fetch data, the data might actually be null
            if (currentNode != null) {
                this.buildField(model, fieldName, currentNode, m);

            }
        } else if (componentType == Component.ComponentType.MULTIMEDIA) {
            if (contentFields.has(fieldName)) {
                this.buildMultimediaField(model, fieldName, contentFields.get(fieldName), m);
            }
        }
    }

    /**
     * Searches for the Json node to set on the model field in the Json data.
     *
     * @param entityFieldName The annotated model property. Used to search the Json node
     * @param rawJsonData     The Json data representing a node inside a child node of a component. Used for
     *                        embedded fields and component link fields
     * @param isRootComponent A flag to check whether the current Json node is the component node. If it is
     *                        the case, then a choice is made whether to fetch the metadata or the normal content
     *                        node.
     * @param contentFields   The content node
     * @param metadataFields  The metadata node
     * @param m               The current model field that is parsing at the moment
     * @return the Json node found under the entityFieldName key or null
     */
    private static JsonNode getJsonNodeToParse(final String entityFieldName, final JsonNode rawJsonData,
            final boolean isRootComponent, final boolean isEmbeddable, final JsonNode contentFields,
            final JsonNode metadataFields, final ModelFieldMapping m) {

        final JsonNode currentNode;
        if (isRootComponent) {
            if (m.getViewModelProperty().isMetadata()) {
                currentNode = metadataFields;
            } else {
                currentNode = contentFields;
            }
        } else {
            currentNode = rawJsonData;
        }

        if (currentNode != null) {
            if (isRootComponent || isEmbeddable) {
                return currentNode.get(entityFieldName);
            }
            return currentNode;
        }
        return null;
    }

    private <T extends BaseViewModel> void buildMultimediaField(final T model, final String fieldName,
            final JsonNode currentField, final ModelFieldMapping modelFieldMapping) throws IllegalAccessException {
        final Field modelField = modelFieldMapping.getField();
        modelField.setAccessible(true);
        setXPathForXpm(model, fieldName, currentField, modelField);

        Class<?> fieldTypeOfFieldToSet = TypeUtils.determineTypeOfField(modelField);

        if (fieldTypeOfFieldToSet == String.class) {
            modelField.set(model, currentField.textValue());
        } else if (fieldTypeOfFieldToSet == int.class || fieldTypeOfFieldToSet == Integer.class) {
            modelField.set(model, currentField.intValue());
        }
    }

    private <T extends BaseViewModel> void buildField(final T model, final String fieldName,
            final JsonNode currentField, final ModelFieldMapping modelFieldMapping)
            throws IllegalAccessException, SerializationException, IOException {

        final Field modelField = modelFieldMapping.getField();
        modelField.setAccessible(true);

        FieldType tridionDataFieldType = FieldType.UNKNOWN;
        if (currentField.has(DataBindConstants.FIELD_TYPE_KEY)) {
            tridionDataFieldType = FieldType
                    .findByValue(currentField.get(DataBindConstants.FIELD_TYPE_KEY).intValue());
        }
        LOG.debug("Tridion field type: {}", tridionDataFieldType);

        Class<?> fieldTypeOfFieldToSet = TypeUtils.determineTypeOfField(modelField);

        boolean modelFieldIsRegularEmbeddedType = FieldSet.class.isAssignableFrom(fieldTypeOfFieldToSet)
                || Embedded.class.isAssignableFrom(fieldTypeOfFieldToSet);

        final List<JsonNode> nodeList = new ArrayList<>();
        if (tridionDataFieldType.equals(FieldType.COMPONENTLINK)
                || tridionDataFieldType.equals(FieldType.MULTIMEDIALINK)) {
            processLinkedFields(model, currentField, modelFieldMapping, modelField, tridionDataFieldType, nodeList);
        } else if (tridionDataFieldType == FieldType.EMBEDDED && !modelFieldIsRegularEmbeddedType) {

            handleEmbeddedContent(currentField, nodeList);
        } else if (tridionDataFieldType == FieldType.UNKNOWN) {
            // we're in the embedded scenario where there is no field type
            // This is where the serializer passes when it's trying to deserialize the actual field in an embedded
            // component.

            // add more info, like the embeddedschema info, XPM info here
            // Best thing to do may be to just add required values to
            // an embedded base class

            if (currentField.has(fieldName)) {
                nodeList.add(currentField.get(fieldName));
            }
        } else {
            nodeList.add(currentField);
        }

        if (nodeList.isEmpty()) {
            LOG.debug("Nothing to do.");
            return;
        }

        setXPathForXpm(model, fieldName, currentField, modelField);

        deserializeAndBuildModels(model, fieldName, modelField, tridionDataFieldType, nodeList);
    }

    private <T extends BaseViewModel> void processLinkedFields(final T model, final JsonNode currentField,
            final ModelFieldMapping modelFieldMapping, final Field modelField, final FieldType tridionDataFieldType,
            final List<JsonNode> nodeList) throws SerializationException, IllegalAccessException {
        if (modelFieldMapping.getViewModelProperty().resolveLinkForComponentLinkField()
                && modelField.getType().getName().equalsIgnoreCase(String.class.getName())) {
            LOG.debug("Resolving link for a component link or multimedialink field. ");

            if (tridionDataFieldType.equals(FieldType.COMPONENTLINK)) {

                if (currentField.has(VALUES_NODE) && currentField.get(VALUES_NODE).hasNonNull(0)) {
                    String componentId = currentField.get(VALUES_NODE).get(0).textValue();
                    String resolved = "";
                    try {
                        resolved = getLinkResolver().resolve(componentId);
                    } catch (ItemNotFoundException e) {
                        LOG.error("Could not resolve link for: " + componentId, e);
                    }
                    modelField.set(model, resolved);
                }
            } else if (tridionDataFieldType.equals(FieldType.MULTIMEDIALINK)) {

                if (currentField.has(LINKED_COMPONENT_VALUES_NODE)
                        && currentField.get(LINKED_COMPONENT_VALUES_NODE).hasNonNull(0)) {

                    JsonNode multiMediaNode = currentField.get(LINKED_COMPONENT_VALUES_NODE).get(0);

                    if (multiMediaNode.hasNonNull(MULTIMEDIA)) {
                        modelField.set(model, multiMediaNode.get(MULTIMEDIA).get(URL).textValue());
                    }
                }
            }
        } else {
            fillLinkedComponentValues(currentField, nodeList);
        }
    }

    private <T extends BaseViewModel> void setXPathForXpm(final T model, final String fieldName,
            final JsonNode currentField, final Field modelField) {
        if (model instanceof TridionViewModel && currentField != null
                && currentField.has(DataBindConstants.XPATH)) {
            boolean isMultiValued = false;
            if (modelField.getType().equals(List.class)) {
                isMultiValued = true;
            }
            String xpath = currentField.get(DataBindConstants.XPATH).asText();

            ((TridionViewModel) model).addXpmEntry(fieldName, xpath, isMultiValued);
        }
    }

    private <T extends BaseViewModel> void deserializeAndBuildModels(final T model, final String fieldName,
            final Field modelField, final FieldType tridionDataFieldType, final List<JsonNode> nodeList)
            throws SerializationException, IllegalAccessException, IOException {
        if (modelField.getType().equals(List.class)) {
            final Type parametrizedType = TypeUtils.getRuntimeTypeOfTypeParameter(modelField.getGenericType());
            LOG.debug("Interface check: " + TypeUtils.classIsViewModel((Class<?>) parametrizedType));

            if (TypeUtils.classIsViewModel((Class<?>) parametrizedType)
                    || databinder.classHasViewModelDerivatives(((Class<?>) parametrizedType).getCanonicalName())) {
                for (JsonNode node : nodeList) {

                    if (!node.has(DataBindConstants.ROOT_ELEMENT_NAME)) {
                        checkTypeAndBuildModel(model, fieldName, node, modelField, (Class<T>) parametrizedType);
                    } else {
                        LOG.debug("not handling a schemaNode.");
                    }
                }

            } else {
                for (JsonNode node : nodeList) {
                    if (!node.has(DataBindConstants.ROOT_ELEMENT_NAME)) {
                        deserializeGeneric(model, node, modelField, tridionDataFieldType);
                    } else {
                        LOG.debug("not handling a schemaNode.");
                    }
                }
            }

        } else if (TypeUtils.classIsViewModel(modelField.getType())
                || databinder.classHasViewModelDerivatives(modelField.getType().getCanonicalName())) {
            final Class<T> modelClassToUse = (Class<T>) modelField.getType();
            checkTypeAndBuildModel(model, fieldName, nodeList.get(0), modelField, modelClassToUse);
        } else {
            deserializeGeneric(model, nodeList.get(0), modelField, tridionDataFieldType);
        }
    }

    private static void handleEmbeddedContent(final JsonNode currentField, final List<JsonNode> nodeList) {
        final JsonNode embeddedNode = currentField.get(DataBindConstants.EMBEDDED_VALUES_NODE);
        // This is a fix for when we are already in an embedded node. The Json unfortunately
        // keeps sibling nodes in this child, which has FieldType embedded, while we're actually already in
        // that node's Values

        final JsonNode schemaNode = currentField.get(DataBindConstants.EMBEDDED_SCHEMA_FIELD_NAME);
        if (embeddedNode != null) {
            final Iterator<JsonNode> embeddedIterator = embeddedNode.elements();
            while (embeddedIterator.hasNext()) {
                addEmbeddedNodeAndSchemaInfo(nodeList, schemaNode, embeddedIterator);
            }
        } else {
            final Iterator<JsonNode> currentFieldElements = currentField.elements();
            while (currentFieldElements.hasNext()) {

                addEmbeddedNodeAndSchemaInfo(nodeList, schemaNode, currentFieldElements);
            }
        }
    }

    private static void addEmbeddedNodeAndSchemaInfo(final List<JsonNode> nodeList, final JsonNode schemaNode,
            final Iterator<JsonNode> embeddedIterator) {
        ObjectNode embeddedValue = (ObjectNode) embeddedIterator.next();

        if (schemaNode != null && !embeddedValue.has(DataBindConstants.EMBEDDED_SCHEMA_FIELD_NAME)) {
            embeddedValue.set(DataBindConstants.EMBEDDED_SCHEMA_FIELD_NAME, schemaNode);
        }
        nodeList.add(embeddedValue);
    }

    private static void fillLinkedComponentValues(final JsonNode currentField, final List<JsonNode> nodeList) {
        // Get the actual values from the values
        // if the Model's field is List, grab all embedded values
        // if it's a normal class (ComponentImpl or similar), just get the first

        final Iterator<JsonNode> nodes = currentField.get(DataBindConstants.LINKED_COMPONENT_VALUES_NODE)
                .elements();
        while (nodes.hasNext()) {
            nodeList.add(nodes.next());
        }
    }

    /**
     * Deserializes in a Strongly Typed Model.
     *
     * @param model           the model to build
     * @param fieldName       the current field name
     * @param currentField    the current Json node
     * @param modelField      the model property
     * @param modelClassToUse the Model class
     * @param <T>             the model class extending from BaseViewModel
     * @throws SerializationException serialization issues
     * @throws IllegalAccessException Class instantiation issues
     */
    private <T extends BaseViewModel> void checkTypeAndBuildModel(final T model, final String fieldName,
            final JsonNode currentField, final Field modelField, final Class<T> modelClassToUse)
            throws SerializationException, IllegalAccessException {
        if (!model.getClass().equals(modelField.getType())) {
            LOG.debug("Building a model or Component for field:{}, type: {}", fieldName,
                    modelField.getType().getName());
            final BaseViewModel strongModel = buildModelForField(currentField, modelClassToUse);

            if (modelField.getType().equals(List.class)) {
                addToListTypeField(model, modelField, strongModel);
            } else {
                modelField.set(model, strongModel);
            }
        } else {
            LOG.error(
                    "Type for field type: {} is the same as the type for this view model: {}. This is NOT supported"
                            + " because of infinite loops. Work around this by creating a separate field type.",
                    model.getClass().getCanonicalName(), modelField.getType().getCanonicalName());
        }
    }

    private <T extends BaseViewModel> BaseViewModel buildModelForField(final JsonNode currentField,
            final Class<T> modelClassToUse) throws SerializationException {

        if (Modifier.isAbstract(modelClassToUse.getModifiers())
                || Modifier.isInterface(modelClassToUse.getModifiers())) {

            // Get root element name
            final String rootElementName = getRootElementNameFromComponentOrEmbeddedField(currentField);
            if (StringUtils.isNotEmpty(rootElementName)) {
                // attempt get a concrete class for this interface

                final Class<? extends BaseViewModel> concreteClass = databinder
                        .getConcreteModel(modelClassToUse.getCanonicalName(), rootElementName);
                if (concreteClass == null) {
                    LOG.error("Attempt to find a concrete model class for interface or abstract class: {} failed "
                            + "miserably as there was no registered class for root element name: '{}' Will return null"
                            + ".", modelClassToUse.getCanonicalName(), rootElementName);
                    return null;
                }
                LOG.debug("Building: {}", concreteClass.getCanonicalName());
                return getBaseViewModel(currentField, concreteClass);
            } else {
                LOG.error(
                        "Attempt to find a concrete model class for interface or abstract class: {} failed "
                                + "miserably as a root element name could not be found. Will return null.",
                        modelClassToUse.getCanonicalName());
                return null;
            }

        } else {

            return getBaseViewModel(currentField, modelClassToUse);
        }
    }

    private String getRootElementNameFromComponentOrEmbeddedField(final JsonNode currentField) {
        final String rootElementName = databinder.getRootElementName(currentField);

        if (StringUtils.isNotEmpty(rootElementName)) {
            return rootElementName;
        }

        if (currentField.has(DataBindConstants.EMBEDDED_SCHEMA_FIELD_NAME)) {
            final JsonNode schemaNode = currentField.get(DataBindConstants.EMBEDDED_SCHEMA_FIELD_NAME);

            if (schemaNode.hasNonNull(DataBindConstants.ROOT_ELEMENT_NAME)) {
                String nodeTypeName = schemaNode.get(DataBindConstants.ROOT_ELEMENT_NAME).textValue();
                LOG.debug("RootElementName is: {}", nodeTypeName);
                return nodeTypeName;
            }
        }
        return null;
    }

    private <T extends BaseViewModel> BaseViewModel getBaseViewModel(final JsonNode currentField,
            final Class<T> modelClassToUse) throws SerializationException {
        final BaseViewModel strongModel = databinder.buildModel(currentField, modelClassToUse, "");
        final ViewModel viewModelParameters = modelClassToUse.getAnnotation(ViewModel.class);
        if (viewModelParameters.setRawData()) {
            strongModel.setRawData(currentField.toString());
        }
        return strongModel;
    }

    private <T extends BaseViewModel> void deserializeGeneric(final T model, final JsonNode currentField,
            final Field f, final FieldType fieldType)
            throws IOException, IllegalAccessException, SerializationException {
        LOG.debug("Field Type: " + f.getType().getCanonicalName());

        if (currentField.has(DataBindConstants.COMPONENT_TYPE)) {
            LOG.debug("Building a linked Component or Multimedia component");
            final Component component = databinder.buildComponent(currentField,
                    databinder.getConcreteComponentImpl());
            setFieldValue(model, f, component, fieldType);
        } else {
            final org.dd4t.contentmodel.Field renderedField = JsonUtils.renderComponentField(currentField,
                    this.concreteFieldImpl);
            LOG.trace("Rendered Field is: {} ", renderedField.toString());
            LOG.debug("Field Type is: {}", f.getType().toString());
            setFieldValue(model, f, renderedField, fieldType);
        }
    }

    private void setTridionProperties(final TridionViewModel model, final JsonNode rawComponent) {
        model.setLastPublishDate(JsonUtils.getDateFromField(DataBindConstants.LAST_PUBLISHED_DATE, rawComponent));
        model.setLastModified(JsonUtils.getDateFromField(DataBindConstants.LAST_MODIFIED_DATE, rawComponent));
        model.setTcmUri(JsonUtils.getTcmUriFromField(DataBindConstants.ID, rawComponent));
    }

    public JsonDataBinder getDatabinder() {
        return databinder;
    }

    public void setDatabinder(JsonDataBinder databinder) {
        this.databinder = databinder;
    }
}