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.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.Collections;
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 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 value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value != null && value.getData() != null) {
gen.writeStartObject();
TypedParams<IncludedFieldsParams> includedFields = value.getResponse()
.getQueryParams()
.getIncludedFields();
TypedParams<IncludedRelationsParams> includedRelations = value.getResponse()
.getQueryParams()
.getIncludedRelations();
IncludedRelationsParams includedRelationsParams = null;
Class<?> dataClass = value.getData().getClass();
String resourceType = resourceRegistry.getResourceType(dataClass);
if (includedRelations != null && includedRelations.getParams().containsKey(resourceType)) {
includedRelationsParams = includedRelations.getParams().get(resourceType);
}
writeData(gen, value.getData(), includedFields, includedRelationsParams);
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,
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<String> notAttributesFields = entry.getResourceInformation().getNotAttributeFields();
writeAttributes(gen, data, includedFields, notAttributesFields);
Set<ResourceField> relationshipFields = getRelationshipFields(resourceType, resourceInformation, includedFields);
writeRelationshipFields(gen, data, relationshipFields, includedRelations);
writeMetaField(gen, data, entry);
writeLinksField(gen, data, entry);
}
private Set<ResourceField> getRelationshipFields(String resourceType, ResourceInformation resourceInformation,
TypedParams<IncludedFieldsParams> includedFields) {
Set<ResourceField> relationshipFields = new HashSet<>();
Optional<Set<String>> 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
* <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 includedFields <i>field</i> 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<IncludedFieldsParams> includedFields,
final Set<String> notAttributesFields)
throws IOException {
String resourceType = resourceRegistry.getResourceType(data.getClass());
final Optional<Set<String>> fields = includedFields(resourceType, includedFields);
Map<String, Object> dataMap;
if (fields.isPresent()) {
Predicate2<Object, PropertyWriter> includeChecker = new Predicate2<Object, PropertyWriter>() {
@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<Map<String, Object>>() {
});
} else {
Predicate2<Object, PropertyWriter> includeChecker = new Predicate2<Object, PropertyWriter>() {
@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<Map<String, Object>>() {
});
}
Attributes attributesObject = new Attributes();
for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
if (entry.getValue() != null)
attributesObject.addAttribute(entry.getKey(), entry.getValue());
}
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. 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 <i>field</i> query param values
* @return true if it should be included in the response, false otherwise
*/
private static Optional<Set<String>> includedFields(String resourceType, TypedParams<IncludedFieldsParams> includedFields) {
IncludedFieldsParams typeIncludedFields = findIncludedFields(includedFields, resourceType);
if (fieldsForOtherResourceSpecified(includedFields, typeIncludedFields)) {
return Optional.of(Collections.<String>emptySet());
} else if (noResourceIncludedFieldsSpecified(typeIncludedFields)) {
return Optional.empty();
} else {
return Optional.of(typeIncludedFields.getParams());
}
}
/**
* Checks if fields for other resource has been specified but not for the processed one
*
* @param includedFields fields to be included
* @param typeIncludedFields resource fields to be included
* @return true if fields for other resource has been specified but not for the processed one, false otherwise
*/
private static boolean fieldsForOtherResourceSpecified(TypedParams<IncludedFieldsParams> includedFields,
IncludedFieldsParams typeIncludedFields) {
return includedFields != null &&
!includedFields.getParams().isEmpty() &&
noResourceIncludedFieldsSpecified(typeIncludedFields);
}
/**
* 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<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,
IncludedRelationsParams includedRelations)
throws IOException {
DataLinksContainer dataLinksContainer = new DataLinksContainer(data, relationshipFields, includedRelations);
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<Container> 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<Object, PropertyWriter> 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;
}
}