<commit_before>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.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
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.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.DataLinksContainer;
import io.katharsis.utils.BeanUtils;
import io.katharsis.utils.PropertyUtils;

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 <i>data</i> field of JSON API response.
 *
 * @see Container
 */
public class ContainerSerializer extends JsonSerializer<Container> {

    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 SELF_FIELD_NAME = "self";

    private final ResourceRegistry resourceRegistry;
    
    private ObjectMapper attributesObjectMapper = null;

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

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

            TypedParams<IncludedFieldsParams> includedFields = value.getResponse()
                .getQueryParams()
                .getIncludedFields();

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

    /**
     * Writes a value. Each serialized container must contain type field whose value is string
     * <a href="http://jsonapi.org/format/#document-structure-resource-types"></a>.
     */
    private void writeData(JsonGenerator gen, Object data, TypedParams<IncludedFieldsParams> includedFields) 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());
        }

        try {
            writeAttributes(gen, data, resourceInformation.getAttributeFields(), includedFields);
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            StringBuilder attributeFieldNames = new StringBuilder();
            for (ResourceField attributeField : resourceInformation.getAttributeFields()) {
                attributeFieldNames.append(attributeField.getUnderlyingName());
                attributeFieldNames.append(" ");
            }
            throw new JsonSerializationException("Error writing basic fields: " +
                attributeFieldNames);
        }

        Set<ResourceField> relationshipFields = getRelationshipFields(resourceType, resourceInformation, includedFields);
        writeRelationshipFields(gen, data, relationshipFields);
        writeLinksField(gen, data);
    }

    private Set<ResourceField> getRelationshipFields(String resourceType, ResourceInformation resourceInformation, TypedParams<IncludedFieldsParams> includedFields) {
        Set<ResourceField> relationshipFields = new HashSet<>();
        for (ResourceField resourceField : resourceInformation.getRelationshipFields()) {
            if (isIncluded(resourceType, includedFields, resourceField)) {
                relationshipFields.add(resourceField);
            }
        }

        return relationshipFields;
    }

    /**
     * The id MUST be written as a string
     * <a href="http://jsonapi.org/format/#document-structure-resource-ids">Resource IDs</a>.
     */
    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 <i>fields</i> query params. It doesn't allow writing
     * <i>null</i> resource attributes.
     * @param gen Jackson generator
     * @param data resource object
     * @param attributeFields resource attribute definitions
     * @param includedFields <i>field</i> query param values
     * @throws IllegalAccessException if couldn't access an attribute
     * @throws InvocationTargetException if couldn't access an attribute
     * @throws NoSuchMethodException if couldn't access an attribute
     * @throws IOException if couldn't write attributes
     */
    private void writeAttributes(JsonGenerator gen, Object data, Set<ResourceField> attributeFields,
                                 TypedParams<IncludedFieldsParams> includedFields)
        throws IllegalAccessException, InvocationTargetException, NoSuchMethodException, IOException {

        String resourceType = resourceRegistry.getResourceType(data.getClass());
        
        Set<String> allowedFields = new HashSet<>();
        for (ResourceField attributeField : attributeFields) {
            if (isIncluded(resourceType, includedFields, attributeField)) {
                allowedFields.add(attributeField.getJsonName());
            }
        }
        
        Map<String, Object> dataMap = getObjectMapper(gen, allowedFields).convertValue(data, new TypeReference<Map<String, Object>>() {});
        Attributes attributesObject = new Attributes();
        for(String key : dataMap.keySet()) {
            Object value = dataMap.get(key);
            if(value != null)
                attributesObject.addAttribute(key, value);
        }

        gen.writeObjectField(ATTRIBUTES_FIELD_NAME, attributesObject);
    }

    /**
     * When <i>fields</i> filter is passed in the query params, <b>attributes</b> and <b>relationships</b> should be
     * filtered accordingly to the requested fields.
     * @param resourceType JSON API name of a resource
     * @param includedFields <i>field</i> query param values
     * @param attributeField resource attribute field
     * @return <i>true</i> if it should be included in the response, <i>false</i> otherwise
     */
    private boolean isIncluded(String resourceType, TypedParams<IncludedFieldsParams> includedFields, ResourceField attributeField) {
        IncludedFieldsParams typeIncludedFields = findIncludedFields(includedFields, resourceType);
        if (typeIncludedFields == null || typeIncludedFields.getParams().isEmpty()) {
            return includedFields == null || includedFields.getParams().isEmpty();
        } else {
            return typeIncludedFields.getParams().contains(attributeField.getJsonName());
        }
    }

    private static IncludedFieldsParams findIncludedFields(TypedParams<IncludedFieldsParams> includedFields, String
        elementName) {
        IncludedFieldsParams includedFieldsParams = null;
        if (includedFields != null) {
            for (Map.Entry<String, IncludedFieldsParams> entry : includedFields.getParams()
                .entrySet()) {
                if (elementName.equals(entry.getKey())) {
                    includedFieldsParams = entry.getValue();
                }
            }
        }
        return includedFieldsParams;
    }

    private static void writeRelationshipFields(JsonGenerator gen, Object data, Set<ResourceField> relationshipFields)
        throws IOException {
        DataLinksContainer dataLinksContainer = new DataLinksContainer(data, relationshipFields);
        gen.writeObjectField(RELATIONSHIPS_FIELD_NAME, dataLinksContainer);
    }

    private void writeLinksField(JsonGenerator gen, Object data) throws IOException {
        gen.writeFieldName(LINKS_FIELD_NAME);
        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);
    }

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

    /**
     Generate a new object mapper that ignore all fields except the specified
     */
    private ObjectMapper getObjectMapper(JsonGenerator gen, Set<String> allowedFields) {
        if(attributesObjectMapper == null) {
            attributesObjectMapper = ((ObjectMapper)gen.getCodec()).copy();
            attributesObjectMapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
                @Override
                public Object findFilterId(Annotated a) {
                    Object filterId = super.findFilterId(a);
                    if (filterId == null) {
                        filterId = "katharsisFilter";
                    }
                    return filterId;
                }
            });

            FilterProvider fp = new SimpleFilterProvider().addFilter("katharsisFilter", SimpleBeanPropertyFilter.filterOutAllExcept(allowedFields));
            attributesObjectMapper.setFilterProvider(fp);
        }
        return attributesObjectMapper;
    }
}<commit_msg>refactoring to #241<commit_after>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.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
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.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.DataLinksContainer;
import io.katharsis.utils.BeanUtils;
import io.katharsis.utils.PropertyUtils;

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 <i>data</i> field of JSON API response.
 *
 * @see Container
 */
public class ContainerSerializer extends JsonSerializer<Container> {

    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 SELF_FIELD_NAME = "self";
    private static final String JACKSON_ATTRIBUTE_FILTER_NAME = "katharsisFilter";
    private static final TypeReference<Map<String, Object>> DEFAULT_MAP = new TypeReference<Map<String, Object>>() {
    };

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

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

            TypedParams<IncludedFieldsParams> includedFields = value.getResponse()
                .getQueryParams()
                .getIncludedFields();

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

    /**
     * Writes a value. Each serialized container must contain type field whose value is string
     * <a href="http://jsonapi.org/format/#document-structure-resource-types"></a>.
     */
    private void writeData(JsonGenerator gen, Object data, TypedParams<IncludedFieldsParams> includedFields) 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());
        }

        try {
            writeAttributes(gen, data, resourceInformation.getAttributeFields(), includedFields);
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            StringBuilder attributeFieldNames = new StringBuilder();
            for (ResourceField attributeField : resourceInformation.getAttributeFields()) {
                attributeFieldNames.append(attributeField.getUnderlyingName());
                attributeFieldNames.append(" ");
            }
            throw new JsonSerializationException("Error writing basic fields: " +
                attributeFieldNames);
        }

        Set<ResourceField> relationshipFields = getRelationshipFields(resourceType, resourceInformation, includedFields);
        writeRelationshipFields(gen, data, relationshipFields);
        writeLinksField(gen, data);
    }

    private Set<ResourceField> getRelationshipFields(String resourceType, ResourceInformation resourceInformation, TypedParams<IncludedFieldsParams> includedFields) {
        Set<ResourceField> relationshipFields = new HashSet<>();
        for (ResourceField resourceField : resourceInformation.getRelationshipFields()) {
            if (isIncluded(resourceType, includedFields, resourceField)) {
                relationshipFields.add(resourceField);
            }
        }

        return relationshipFields;
    }

    /**
     * The id MUST be written as a string
     * <a href="http://jsonapi.org/format/#document-structure-resource-ids">Resource IDs</a>.
     */
    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 <i>fields</i> query params. It doesn't allow writing
     * <i>null</i> resource attributes.
     * @param gen Jackson generator
     * @param data resource object
     * @param attributeFields resource attribute definitions
     * @param includedFields <i>field</i> query param values
     * @throws IllegalAccessException if couldn't access an attribute
     * @throws InvocationTargetException if couldn't access an attribute
     * @throws NoSuchMethodException if couldn't access an attribute
     * @throws IOException if couldn't write attributes
     */
    private void writeAttributes(JsonGenerator gen, Object data, Set<ResourceField> attributeFields,
                                 TypedParams<IncludedFieldsParams> includedFields)
        throws IllegalAccessException, InvocationTargetException, NoSuchMethodException, IOException {

        String resourceType = resourceRegistry.getResourceType(data.getClass());
        
        Set<String> allowedFields = new HashSet<>(attributeFields.size());
        for (ResourceField attributeField : attributeFields) {
            if (isIncluded(resourceType, includedFields, attributeField)) {
                allowedFields.add(attributeField.getJsonName());
            }
        }

        Map<String, Object> dataMap = getObjectMapper(gen, allowedFields)
            .convertValue(data, DEFAULT_MAP);
        Attributes attributesObject = new Attributes();
        for(String key : dataMap.keySet()) {
            Object value = dataMap.get(key);
            if(value != null)
                attributesObject.addAttribute(key, value);
        }

        gen.writeObjectField(ATTRIBUTES_FIELD_NAME, attributesObject);
    }

    /**
     * When <i>fields</i> filter is passed in the query params, <b>attributes</b> and <b>relationships</b> should be
     * filtered accordingly to the requested fields.
     * @param resourceType JSON API name of a resource
     * @param includedFields <i>field</i> query param values
     * @param attributeField resource attribute field
     * @return <i>true</i> if it should be included in the response, <i>false</i> otherwise
     */
    private boolean isIncluded(String resourceType, TypedParams<IncludedFieldsParams> includedFields, ResourceField attributeField) {
        IncludedFieldsParams typeIncludedFields = findIncludedFields(includedFields, resourceType);
        if (typeIncludedFields == null || typeIncludedFields.getParams().isEmpty()) {
            return includedFields == null || includedFields.getParams().isEmpty();
        } else {
            return typeIncludedFields.getParams().contains(attributeField.getJsonName());
        }
    }

    private static IncludedFieldsParams findIncludedFields(TypedParams<IncludedFieldsParams> includedFields, String
        elementName) {
        IncludedFieldsParams includedFieldsParams = null;
        if (includedFields != null) {
            for (Map.Entry<String, IncludedFieldsParams> entry : includedFields.getParams()
                .entrySet()) {
                if (elementName.equals(entry.getKey())) {
                    includedFieldsParams = entry.getValue();
                }
            }
        }
        return includedFieldsParams;
    }

    private static void writeRelationshipFields(JsonGenerator gen, Object data, Set<ResourceField> relationshipFields)
        throws IOException {
        DataLinksContainer dataLinksContainer = new DataLinksContainer(data, relationshipFields);
        gen.writeObjectField(RELATIONSHIPS_FIELD_NAME, dataLinksContainer);
    }

    private void writeLinksField(JsonGenerator gen, Object data) throws IOException {
        gen.writeFieldName(LINKS_FIELD_NAME);
        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);
    }

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

    /**
     Generate a new object mapper that ignore all fields except the specified
     */
    private ObjectMapper getObjectMapper(JsonGenerator gen, Set<String> allowedFields) {
        ObjectMapper attributesObjectMapper = ((ObjectMapper)gen.getCodec())
            .copy();
        attributesObjectMapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
            @Override
            public Object findFilterId(Annotated a) {
                Object filterId = super.findFilterId(a);
                if (filterId == null) {
                    filterId = JACKSON_ATTRIBUTE_FILTER_NAME;
                }
                return filterId;
            }
        });

        FilterProvider fp = new SimpleFilterProvider()
            .addFilter(JACKSON_ATTRIBUTE_FILTER_NAME, SimpleBeanPropertyFilter.filterOutAllExcept(allowedFields));
        attributesObjectMapper.setFilterProvider(fp);

        return attributesObjectMapper;
    }
}
{'fraction_non_alphanumeric': 0.08538434100231854, 'fraction_numerical': 0.00013376136971642589, 'mean_word_length': 3.365317244063838, 'pattern_counts': {'":': 0, '<': 99, '<?xml version=': 0, '>': 99, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 20}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1374372', 'n_tokens_mistral': 5576, 'n_tokens_neox': 5388, 'n_words': 1277}