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

com.toedter.spring.hateoas.jsonapi.JsonApiData Maven / Gradle / Ivy

/*
 * Copyright 2020 the original author or authors.
 *
 * 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
 *
 *      https://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 com.toedter.spring.hateoas.jsonapi;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Value;
import lombok.With;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.PagedModel;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.mediatype.PropertyUtils;
import org.springframework.lang.Nullable;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import static com.toedter.spring.hateoas.jsonapi.ReflectionUtils.getAllDeclaredFields;

@Value
@Getter(onMethod_ = {@JsonProperty})
@With(AccessLevel.PACKAGE)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
class JsonApiData {
    String id;
    String type;
    Map attributes;
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    Object relationships;
    Links links;
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    Map meta;
    private static final ObjectMapper objectMapper = new ObjectMapper();

    @JsonCreator
    public JsonApiData(
            @JsonProperty("id") @Nullable String id,
            @JsonProperty("type") @Nullable String type,
            @JsonProperty("attributes") @Nullable Map attributes,
            @JsonProperty("relationships") @Nullable Object relationships,
            @JsonProperty("links") @Nullable Links links,
            @JsonProperty("meta") @Nullable Map meta
    ) {
        this.id = id;
        this.type = type;
        this.attributes = attributes;
        this.relationships = relationships;
        this.links = links;
        this.meta = meta;
    }

    public JsonApiData() {
        this(null, null, null, null, null, null);
    }

    public static List extractCollectionContent(
            CollectionModel collectionModel,
            JsonApiConfiguration jsonApiConfiguration,
            @Nullable HashMap> sparseFieldsets,
            boolean eliminateDuplicates) {

        if (eliminateDuplicates) {
            HashMap values = new HashMap<>();
            for (Object entity : collectionModel.getContent()) {
                Optional jsonApiData =
                        extractContent(entity, false, jsonApiConfiguration, sparseFieldsets);
                jsonApiData.ifPresent(apiData -> values.put(apiData.getId() + "." + apiData.getType(), apiData));
            }
            return new ArrayList<>(values.values());
        } else {
            List dataList = new ArrayList<>();
            for (Object entity : collectionModel.getContent()) {
                Optional jsonApiData =
                        extractContent(entity, false, jsonApiConfiguration, sparseFieldsets);
                jsonApiData.ifPresent(dataList::add);
            }
            return dataList;
        }
    }

    public static Optional extractContent(
            @Nullable Object content,
            boolean isSingleEntity,
            JsonApiConfiguration jsonApiConfiguration,
            @Nullable HashMap> sparseFieldsets) {

        Links links = null;
        Map relationships = null;
        Map metaData = null;

        if (content instanceof RepresentationModel) {
            links = ((RepresentationModel) content).getLinks();
        }

        if (content instanceof JsonApiModel) {
            JsonApiModel jsonApiRepresentationModel = (JsonApiModel) content;
            relationships = jsonApiRepresentationModel.getRelationships();
            sparseFieldsets = jsonApiRepresentationModel.getSparseFieldsets();
            metaData = jsonApiRepresentationModel.getMetaData();
            content = jsonApiRepresentationModel.getContent();
        }

        if (content instanceof EntityModel) {
            content = ((EntityModel) content).getContent();
        }

        if (content == null) {
            // will lead to "data":null, which is compliant with the JSON:API spec
            return Optional.empty();
        }

        final Field[] fields = getAllDeclaredFields(content.getClass());
        if (fields.length == 0
                || (content instanceof RepresentationModel && fields.length == 1)
                || (content instanceof RepresentationModel && fields.length == 2
                && ("$jacocoData".equals(fields[0].getName()) || "$jacocoData".equals(fields[1].getName())))) {
            return Optional.empty();
        }

        JsonApiResourceIdentifier.ResourceField idField;
        idField = JsonApiResourceIdentifier.getId(content, jsonApiConfiguration);

        if (isSingleEntity || links != null && links.isEmpty()) {
            links = null;
        }
        JsonApiResourceIdentifier.ResourceField typeField = JsonApiResourceIdentifier.getType(content, jsonApiConfiguration);

        // This does not serialize attribute values but leaves the original objects
        Map attributeMapFromPropertyUtils = PropertyUtils.extractPropertyValues(content);

        Map attributeMap = objectMapper.convertValue(content, Map.class);

        // we want to use the map created by the object mapper to react on Jackson annotations like
        // @JsonInclude(JsonInclude.Include.NON_NULL)
        for (String key : attributeMapFromPropertyUtils.keySet()) {
            if (attributeMap.containsKey(key)) {
                attributeMap.put(key, attributeMapFromPropertyUtils.get(key));
            }
        }

        attributeMap.remove("links");
        attributeMap.remove(idField.name);
        attributeMap.remove(typeField.name);

        Links finalLinks = links;
        String finalId = idField.value;
        String finalType = typeField.value;
        Map finalRelationships = relationships;
        Map finalMetaData = metaData;

        // apply sparse fieldsets
        if (sparseFieldsets != null) {
            Collection attributes = sparseFieldsets.get(finalType);
            if (attributes != null) {
                Set keys = new HashSet<>(attributeMap.keySet());
                for (String key : keys) {
                    if (!attributes.contains(key)) {
                        attributeMap.remove(key);
                    }
                }
            }
        }
        final Map finalContentObject = attributeMap;

        return Optional.of(content)
                .filter(it -> !RESOURCE_TYPES.contains(it.getClass()))
                .map(it -> new JsonApiData(
                        finalId, finalType, finalContentObject, finalRelationships, finalLinks, finalMetaData));
    }

    static final HashSet> RESOURCE_TYPES = new HashSet<>(
            Arrays.asList(RepresentationModel.class, EntityModel.class, CollectionModel.class, PagedModel.class));
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy