All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.katharsis.jackson.serializer.ContainerSerializer Maven / Gradle / Ivy

There is a newer version: 3.0.2
Show newest version
package io.katharsis.jackson.serializer;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import io.katharsis.jackson.exception.JsonSerializationException;
import io.katharsis.queryParams.params.IncludedFieldsParams;
import io.katharsis.queryParams.params.IncludedRelationsParams;
import io.katharsis.queryParams.params.TypedParams;
import io.katharsis.request.dto.Attributes;
import io.katharsis.resource.field.ResourceField;
import io.katharsis.resource.information.ResourceInformation;
import io.katharsis.resource.registry.RegistryEntry;
import io.katharsis.resource.registry.ResourceRegistry;
import io.katharsis.response.Container;
import io.katharsis.response.ContainerType;
import io.katharsis.response.DataLinksContainer;
import io.katharsis.utils.BeanUtils;
import io.katharsis.utils.Predicate2;
import io.katharsis.utils.PropertyUtils;
import io.katharsis.utils.java.Optional;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * This class serializes an single resource which can be included in data field of JSON API response.
 *
 * @see Container
 */
public class ContainerSerializer extends JsonSerializer {

    private static final String TYPE_FIELD_NAME = "type";
    private static final String ID_FIELD_NAME = "id";
    private static final String ATTRIBUTES_FIELD_NAME = "attributes";
    private static final String RELATIONSHIPS_FIELD_NAME = "relationships";
    private static final String LINKS_FIELD_NAME = "links";
    private static final String META_FIELD_NAME = "meta";
    private static final String SELF_FIELD_NAME = "self";
    private static final String JACKSON_ATTRIBUTE_FILTER_NAME = "katharsisFilter";

    private final ResourceRegistry resourceRegistry;

    public ContainerSerializer(ResourceRegistry resourceRegistry) {
        this.resourceRegistry = resourceRegistry;
    }

    @Override
    public void serialize(Container container, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (container != null && container.getData() != null) {
            gen.writeStartObject();

            TypedParams includedFields = null;
            IncludedRelationsParams includedRelationsParams = null;
            if (container.getResponse().getQueryParams() != null) {
                includedFields = container.getResponse()
                        .getQueryParams()
                        .getIncludedFields();
                TypedParams includedRelations = container.getResponse()
                        .getQueryParams()
                        .getIncludedRelations();

                Class dataClass = container.getData().getClass();
                String resourceType = resourceRegistry.getResourceType(dataClass);
                if (includedRelations != null &&
                        includedRelations.getParams().containsKey(resourceType)) {
                    includedRelationsParams = includedRelations.getParams().get(resourceType);
                } else if (includedRelations != null &&
                        container.getContainerType() != null &&
                        container.getContainerType().equals(ContainerType.INCLUDED)) {
                    for (IncludedRelationsParams includedRelationsParamsInner : includedRelations.getParams().values()) {
                        if (includedRelationsParamsInner.getParams().iterator().next().getPathList().get(0).equals(container.getIncludedFieldName())) {
                            includedRelationsParams = includedRelationsParamsInner;
                        }
                    }
                }
            }

            writeData(container, gen, container.getData(), includedFields, includedRelationsParams);
            gen.writeEndObject();
        } else {
            gen.writeObject(null);
        }
    }

    /**
     * Writes a value. Each serialized container must contain type field whose value is string
     * .
     */
    private void writeData(Container container, JsonGenerator gen, Object data, TypedParams includedFields,
                           IncludedRelationsParams includedRelations) throws IOException {
        Class dataClass = data.getClass();
        String resourceType = resourceRegistry.getResourceType(dataClass);

        gen.writeStringField(TYPE_FIELD_NAME, resourceType);

        RegistryEntry entry = resourceRegistry.getEntry(dataClass);
        ResourceInformation resourceInformation = entry.getResourceInformation();
        try {
            writeId(gen, data, resourceInformation.getIdField());
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            throw new JsonSerializationException(
                    "Error writing id field: " + resourceInformation.getIdField().getUnderlyingName());
        }

        Set notAttributesFields = entry.getResourceInformation().getNotAttributeFields();
        writeAttributes(gen, data, includedFields, notAttributesFields);

        Set relationshipFields = getRelationshipFields(resourceType, resourceInformation, includedFields);

        if (!relationshipFields.isEmpty()) {
            writeRelationshipFields(container, gen, data, relationshipFields, includedRelations);
        }
        writeMetaField(gen, data, entry);
        writeLinksField(gen, data, entry);
    }

    private Set getRelationshipFields(String resourceType, ResourceInformation resourceInformation,
                                                     TypedParams includedFields) {
        Set relationshipFields = new HashSet<>();
        Optional> fields = includedFields(resourceType, includedFields);
        if (fields.isPresent()) {
            for (ResourceField resourceField : resourceInformation.getRelationshipFields()) {
                if (fields.get().contains(resourceField.getJsonName())) {
                    relationshipFields.add(resourceField);
                }
            }
        } else {
            relationshipFields.addAll(resourceInformation.getRelationshipFields());
        }

        return relationshipFields;
    }

    /**
     * The id MUST be written as a string
     * Resource IDs.
     */
    private static void writeId(JsonGenerator gen, Object data, ResourceField idField)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException, IOException {
        String sourceId = BeanUtils.getProperty(data, idField.getUnderlyingName());
        gen.writeObjectField(ID_FIELD_NAME, sourceId);
    }

    /**
     * Writes resource attributes object taking into account fields query params. It doesn't allow writing
     * null resource attributes.
     *
     * @param gen                 Jackson generator
     * @param data                resource object
     * @param includedFields      field query param values
     * @param notAttributesFields names of relationships and id field
     * @throws IOException if couldn't write attributes
     */
    private void writeAttributes(JsonGenerator gen, final Object data, TypedParams includedFields,
                                 final Set notAttributesFields)
            throws IOException {

        String resourceType = resourceRegistry.getResourceType(data.getClass());

        final Optional> fields = includedFields(resourceType, includedFields);

        Map dataMap;
        if (fields.isPresent()) {
            Predicate2 includeChecker = new Predicate2() {
                @Override
                public boolean test(Object bean, PropertyWriter writer) {
                    return bean != data || (fields.get().contains(writer.getName()) &&
                            !notAttributesFields.contains(writer.getName()));
                }
            };
            ObjectMapper om = getObjectMapper(gen, data, includeChecker);
            dataMap = om.convertValue(data, new TypeReference>() {
            });
        } else {
            Predicate2 includeChecker = new Predicate2() {
                @Override
                public boolean test(Object bean, PropertyWriter writer) {
                    return bean != data || !notAttributesFields.contains(writer.getName());
                }
            };
            ObjectMapper om = getObjectMapper(gen, data, includeChecker);
            dataMap = om.convertValue(data, new TypeReference>() {
            });
        }


        Attributes attributesObject = new Attributes();
        for (Map.Entry entry : dataMap.entrySet()) {
            attributesObject.addAttribute(entry.getKey(), entry.getValue());
        }

        gen.writeObjectField(ATTRIBUTES_FIELD_NAME, attributesObject);
    }

    /**
     * When fields filter is passed in the query params, attributes and relationships should be
     * filtered accordingly to the requested fields. If there are included fields defined for other resources but not
     * for the current one, empty set is returned
     *
     * @param resourceType   JSON API name of a resource
     * @param includedFields field query param values
     * @return true if it should be included in the response, false otherwise
     */
    private static Optional> includedFields(String resourceType, TypedParams includedFields) {
        IncludedFieldsParams typeIncludedFields = findIncludedFields(includedFields, resourceType);
        if (noResourceIncludedFieldsSpecified(typeIncludedFields)) {
            return Optional.empty();
        } else {
            return Optional.of(typeIncludedFields.getParams());
        }
    }

    /**
     * Checks if a value has included fields for a resource
     *
     * @param typeIncludedFields found fields set to be checked
     * @return true if there are no resource fields for inclusion, false otherwise
     */
    private static boolean noResourceIncludedFieldsSpecified(IncludedFieldsParams typeIncludedFields) {
        return typeIncludedFields == null || typeIncludedFields.getParams().isEmpty();
    }

    /**
     * Returns included elements for a resource
     *
     * @param includedFields included fields from request
     * @param elementName    resource name
     * @return included field params
     */
    private static IncludedFieldsParams findIncludedFields(TypedParams includedFields, String
            elementName) {
        IncludedFieldsParams includedFieldsParams = null;
        if (includedFields != null) {
            for (Map.Entry entry : includedFields.getParams()
                    .entrySet()) {
                if (elementName.equals(entry.getKey())) {
                    includedFieldsParams = entry.getValue();
                }
            }
        }
        return includedFieldsParams;
    }

    private static void writeRelationshipFields(Container container, JsonGenerator gen, Object data, Set relationshipFields,
                                                IncludedRelationsParams includedRelations)
            throws IOException {
        DataLinksContainer dataLinksContainer = new DataLinksContainer(data, relationshipFields, includedRelations, container.getContainerType(), container.getIncludedFieldName());
        gen.writeObjectField(RELATIONSHIPS_FIELD_NAME, dataLinksContainer);
    }

    private void writeLinksField(JsonGenerator gen, Object data, RegistryEntry entry) throws IOException {
        gen.writeFieldName(LINKS_FIELD_NAME);
        if (entry.getResourceInformation().getLinksFieldName() != null) {
            gen.writeObject(PropertyUtils.getProperty(data, entry.getResourceInformation().getLinksFieldName()));
        } else {
            gen.writeStartObject();
            writeSelfLink(gen, data);
            gen.writeEndObject();
        }
    }

    private void writeSelfLink(JsonGenerator gen, Object data) throws IOException {
        Class sourceClass = data.getClass();
        String resourceUrl = resourceRegistry.getResourceUrl(sourceClass);
        RegistryEntry entry = resourceRegistry.getEntry(sourceClass);
        ResourceField idField = entry.getResourceInformation().getIdField();

        Object sourceId = PropertyUtils.getProperty(data, idField.getUnderlyingName());
        gen.writeStringField(SELF_FIELD_NAME, resourceUrl + "/" + sourceId);
    }

    private void writeMetaField(JsonGenerator gen, Object data, RegistryEntry entry) throws IOException {
        if (entry.getResourceInformation().getMetaFieldName() != null) {
            gen.writeFieldName(META_FIELD_NAME);
            gen.writeObject(PropertyUtils.getProperty(data, entry.getResourceInformation().getMetaFieldName()));
        }
    }

    public Class handledType() {
        return Container.class;
    }

    /**
     * Generate a new object mapper and configure the filter to exclude some properties.
     */
    private static ObjectMapper getObjectMapper(JsonGenerator gen, final Object data,
                                                Predicate2 includedFields) {
        ObjectMapper attributesObjectMapper = ((ObjectMapper) gen.getCodec())
                .copy();

        FilterProvider fp = new SimpleFilterProvider()
                .addFilter(JACKSON_ATTRIBUTE_FILTER_NAME, new KatharsisFieldPropertyFilter(includedFields));
        attributesObjectMapper.setFilters(fp);

        attributesObjectMapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
            @Override
            public Object findFilterId(Annotated a) {
                Object filterId = null;

                if (a instanceof AnnotatedClass) {
                    AnnotatedClass ac = (AnnotatedClass) a;
                    if (ac.getRawType().equals(data.getClass())) {
                        filterId = JACKSON_ATTRIBUTE_FILTER_NAME;
                    }
                }
                return filterId;
            }
        });

        return attributesObjectMapper;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy