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

com.yahoo.elide.swagger.converter.JsonApiModelResolver Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2021, Yahoo Inc.
 * Licensed under the Apache License, Version 2.0
 * See LICENSE file in project root for terms.
 */
package com.yahoo.elide.swagger.converter;

import com.yahoo.elide.annotation.CreatePermission;
import com.yahoo.elide.annotation.DeletePermission;
import com.yahoo.elide.annotation.Include;
import com.yahoo.elide.annotation.ReadPermission;
import com.yahoo.elide.annotation.UpdatePermission;
import com.yahoo.elide.core.dictionary.EntityDictionary;
import com.yahoo.elide.core.type.ClassType;
import com.yahoo.elide.core.type.Type;
import com.yahoo.elide.core.utils.coerce.converters.Serde;
import com.yahoo.elide.swagger.models.media.Relationship;
import com.yahoo.elide.swagger.models.media.Resource;
import com.fasterxml.jackson.databind.type.SimpleType;
import com.google.common.base.Preconditions;
import org.apache.commons.lang3.StringUtils;

import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverter;
import io.swagger.v3.core.converter.ModelConverterContext;
import io.swagger.v3.core.jackson.ModelResolver;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
import io.swagger.v3.oas.models.media.Schema;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Swagger ModelResolvers map POJO classes to Swagger example.models.
 * This resolver maps the POJO to a JSON-API Resource.
 */
public class JsonApiModelResolver extends ModelResolver {
    private final EntityDictionary dictionary;

    public JsonApiModelResolver(EntityDictionary dictionary) {
        super(Json.mapper());
        this.dictionary = dictionary;
    }

    @Override
    public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context,
            Iterator next) {
        java.lang.reflect.Type type = annotatedType.getType();
        if (!(type instanceof Class || type instanceof SimpleType || type instanceof Type)) {
            return super.resolve(annotatedType, context, next);
        }

        Type clazzType = null;

        /*
         * If an Elide entity is an attribute somewhere in a model, the ModelResolver will
         * end up wrapping this as a SimpleType (rather than trying to resolve the entity class directly).
         */
        if (type instanceof SimpleType) {
            type = ((SimpleType) type).getRawClass();
            clazzType = ClassType.of((Class) type);
        } else if (type instanceof Type) {
            clazzType = (Type) type;
        } else if (type instanceof Class) {
            clazzType = ClassType.of((Class) type);
        }

        /* Not an entity managed by Elide, let Swagger convert it */
        if (!dictionary.hasBinding(clazzType)
                || !dictionary.getEntityBinding(clazzType).isElideModel()) {
            return super.resolve(annotatedType, context, next);
        }

        return getEntitySchema(clazzType, context, next);
    }

    private Resource getEntitySchema(final Type clazzType,
                                     final ModelConverterContext context,
                                     final Iterator next) {
        Resource entitySchema = new Resource();
        entitySchema.name(dictionary.getJsonAliasFor(clazzType));
        entitySchema.description(getSchemaDescription(clazzType));
        entitySchema.setSecurityDescription(getClassPermissions(clazzType));

        Include include = getInclude(clazzType);
        if (include != null && !StringUtils.isBlank(include.friendlyName())) {
            entitySchema.setTitle(include.friendlyName());
        }
        io.swagger.v3.oas.annotations.media.Schema schema = getSchema(clazzType);
        if (schema != null && !StringUtils.isBlank(schema.title())) {
            entitySchema.setTitle(schema.title());
        }

        /* Populate */
        populateAttributes(entitySchema, clazzType, context, next);
        populateRelationships(entitySchema, clazzType);

        return entitySchema;
    }
    private void populateAttributes(final Resource entitySchema, final Type clazzType,
                                    final ModelConverterContext context,
                                    final Iterator next) {
        List requiredAttributes = new ArrayList<>();
        List attributeNames = dictionary.getAttributes(clazzType);
        for (String attributeName : attributeNames) {
            Type attributeType = dictionary.getType(clazzType, attributeName);

            Schema attribute = processAttribute(clazzType, attributeName, attributeType,
                    context, next, requiredAttributes);
            entitySchema.addAttribute(attributeName, attribute);
        }
        if (!requiredAttributes.isEmpty()) {
            entitySchema.getAttributes().required(requiredAttributes);
        }
    }

    private void populateRelationships(final Resource entitySchema, final Type clazzType) {
        List requiredRelationships = new ArrayList<>();
        List relationshipNames = dictionary.getRelationships(clazzType);
        for (String relationshipName : relationshipNames) {

            Type relationshipType = dictionary.getParameterizedType(clazzType, relationshipName);

            Relationship relationship = processRelationship(clazzType, relationshipName, relationshipType,
                    requiredRelationships);

            if (relationship != null) {
                entitySchema.addRelationship(relationshipName, relationship);
            }
        }
        if (!requiredRelationships.isEmpty()) {
            entitySchema.getRelationships().required(requiredRelationships);
        }

        entitySchema.name(getSchemaName(clazzType));

        Include include = getInclude(clazzType);
        if (include != null) {
            if (!StringUtils.isBlank(include.friendlyName())) {
                entitySchema.setTitle(include.friendlyName());
            }
        }
        io.swagger.v3.oas.annotations.media.Schema schema = getSchema(clazzType);
        if (schema != null) {
            if (!StringUtils.isBlank(schema.title())) {
                entitySchema.setTitle(schema.title());
            }
        }
    }

    protected String getSchemaName(Type type) {
        String schemaName = dictionary.getJsonAliasFor(type);
        String apiVersion = EntityDictionary.getModelVersion(type);
        if (!EntityDictionary.NO_VERSION.equals(apiVersion)) {
            schemaName = "v" + apiVersion + "_" + schemaName;
        }
        return schemaName;
    }

    @SuppressWarnings("rawtypes")
    private Class getSerdeSerializedClass(Serde serde) {
        // Gets the serde interface type argument
        Class attributeTypeClass = Object.class;

        try {
            for (java.lang.reflect.Type type : serde.getClass().getGenericInterfaces()) {
                if (type instanceof java.lang.reflect.ParameterizedType parameterizedType) {
                    if (Serde.class.equals(parameterizedType.getRawType())) {
                        attributeTypeClass = (Class) parameterizedType.getActualTypeArguments()[0];
                    }
                }
            }
        } catch (RuntimeException e) {
            // Do nothing
        }

        // Using Object.class as the type argument isn't very helpful so try to get the return type
        try {
            if (Object.class.equals(attributeTypeClass)) {
                for (Method method : serde.getClass().getDeclaredMethods()) {
                    if ("serialize".equals(method.getName())) {
                        Class returnType = method.getReturnType();
                        if (!Object.class.equals(returnType)) {
                           return returnType;
                        }
                    }
                }
            }
        } catch (RuntimeException e) {
            // Do nothing
        }
        return attributeTypeClass;
    }

    @SuppressWarnings("rawtypes")
    private Schema processAttribute(Type clazzType, String attributeName, Type attributeType,
        ModelConverterContext context, Iterator next, List required) {

        Preconditions.checkState(attributeType instanceof ClassType);
        Class attributeTypeClass = ((ClassType) attributeType).getCls();

        Serde serde = dictionary.getSerdeLookup().apply(attributeTypeClass);
        if (serde != null) {
            attributeTypeClass = getSerdeSerializedClass(serde);
        }

        Schema attribute = super.resolve(new AnnotatedType().type(attributeTypeClass), context, next);
        if (attribute == null) {
            attribute = super.resolve(new AnnotatedType().resolveAsRef(true).type(attributeTypeClass), context, next);
        }
        String description = getFieldDescription(clazzType, attributeName);
        String permissions = getFieldPermissions(clazzType, attributeName);

        attribute.setDescription(StringUtils.defaultIfEmpty(joinNonEmpty("\n", description, permissions), null));
        attribute.setExample(StringUtils.defaultIfEmpty(getFieldExample(clazzType, attributeName), null));
        attribute.setReadOnly(getFieldReadOnly(clazzType, attributeName));
        attribute.setWriteOnly(getFieldWriteOnly(clazzType, attributeName));
        attribute.setRequired(getFieldRequiredProperties(clazzType, attributeName));

        if (getFieldRequired(clazzType, attributeName)) {
            required.add(attributeName);
        }
        return attribute;
    }

    private Relationship processRelationship(Type clazz, String relationshipName, Type relationshipClazz,
            List required) {
        Relationship relationship = null;
        try {
            relationship = new Relationship(dictionary.getJsonAliasFor(relationshipClazz));

        /* Skip the relationship if it is not bound in the dictionary */
        } catch (IllegalArgumentException e) {
            return relationship;
        }

        String description = getFieldDescription(clazz, relationshipName);
        String permissions = getFieldPermissions(clazz, relationshipName);

        relationship.setDescription(StringUtils.defaultIfEmpty(joinNonEmpty("\n", description, permissions), null));
        relationship.setExample(StringUtils.defaultIfEmpty(getFieldExample(clazz, relationshipName), null));
        relationship.setReadOnly(getFieldReadOnly(clazz, relationshipName));
        relationship.setWriteOnly(getFieldWriteOnly(clazz, relationshipName));
        relationship.setRequired(getFieldRequiredProperties(clazz, relationshipName));

        if (getFieldRequired(clazz, relationshipName)) {
            required.add(relationshipName);
        }
        return relationship;
    }

    private Include getInclude(Type clazz) {
        return dictionary.getAnnotation(clazz, Include.class);
    }

    private io.swagger.v3.oas.annotations.media.Schema getSchema(Type clazz) {
        return dictionary.getAnnotation(clazz, io.swagger.v3.oas.annotations.media.Schema.class);
    }

    private String getSchemaDescription(Type clazz) {
        io.swagger.v3.oas.annotations.media.Schema schema = getSchema(clazz);
        if (schema == null) {

            String description = EntityDictionary.getEntityDescription(clazz);

            if (StringUtils.isEmpty(description)) {
                return null;
            }

            return description;
        }
        return schema.description();
    }

    private io.swagger.v3.oas.annotations.media.Schema getSchema(Type clazz, String fieldName) {
        return dictionary.getAttributeOrRelationAnnotation(clazz, io.swagger.v3.oas.annotations.media.Schema.class,
                fieldName);
    }

    private List getFieldRequiredProperties(Type clazz, String fieldName) {
        io.swagger.v3.oas.annotations.media.Schema property = getSchema(clazz, fieldName);
        return property != null ? Arrays.asList(property.requiredProperties()) : Collections.emptyList();
    }

    @SuppressWarnings("deprecation")
    private boolean getFieldRequired(Type clazz, String fieldName) {
        io.swagger.v3.oas.annotations.media.Schema property = getSchema(clazz, fieldName);
        return property != null && (RequiredMode.REQUIRED.equals(property.requiredMode()) || property.required());
    }

    @SuppressWarnings("deprecation")
    private boolean getFieldReadOnly(Type clazz, String fieldName) {
        io.swagger.v3.oas.annotations.media.Schema property = getSchema(clazz, fieldName);
        return property != null && (AccessMode.READ_ONLY.equals(property.accessMode()) || property.readOnly());
    }

    @SuppressWarnings("deprecation")
    private boolean getFieldWriteOnly(Type clazz, String fieldName) {
        io.swagger.v3.oas.annotations.media.Schema property = getSchema(clazz, fieldName);
        return property != null && (AccessMode.WRITE_ONLY.equals(property.accessMode()) || property.writeOnly());
    }

    private String getFieldExample(Type clazz, String fieldName) {
        io.swagger.v3.oas.annotations.media.Schema property = getSchema(clazz, fieldName);
        return property == null ? "" : property.example();
    }

    private String getFieldDescription(Type clazz, String fieldName) {
        io.swagger.v3.oas.annotations.media.Schema property = getSchema(clazz, fieldName);
        return property == null ? "" : property.description();
    }

    /**
     * Get the class-level permission annotation value.
     *
     * @param clazz the entity class
     * @return the create and delete permissions for the entity class.
     */
    protected String getClassPermissions(Type clazz) {
        String createPermissions = getCreatePermission(clazz);
        String deletePermissions = getDeletePermission(clazz);

        createPermissions = (createPermissions == null) ? "" : "Create Permissions : (" + createPermissions + ")";
        deletePermissions = (deletePermissions == null) ? "" : "Delete Permissions : (" + deletePermissions + ")";
        return joinNonEmpty("\n", createPermissions, deletePermissions);
    }

    private String joinNonEmpty(String delimiter, String... elements) {
        return Arrays.stream(elements).filter(StringUtils::isNotBlank)
            .collect(Collectors.joining(delimiter));
    }

    /**
     * Get the field level permission annotation value.
     *
     * @param clazz the entity class
     * @param fieldName the field
     * @return read and update permissions for a field.
     */
    protected String getFieldPermissions(Type clazz, String fieldName) {
        String readPermissions = getReadPermission(clazz, fieldName);
        String updatePermissions = getUpdatePermission(clazz, fieldName);

        readPermissions = (readPermissions == null) ? "" : "Read Permissions : (" + readPermissions + ")";
        updatePermissions = (updatePermissions == null) ? "" : "Update Permissions : (" + updatePermissions + ")";
        return joinNonEmpty("\n", readPermissions, updatePermissions);
    }

    /**
     * Get the calculated {@link ReadPermission} value for the field.
     *
     * @param clazz the entity class
     * @param fieldName the field
     * @return the read permissions for a field
     */
    protected String getReadPermission(Type clazz, String fieldName) {
        ReadPermission classPermission = dictionary.getAnnotation(clazz, ReadPermission.class);
        ReadPermission fieldPermission = dictionary.getAttributeOrRelationAnnotation(clazz, ReadPermission.class,
                fieldName);

        if (fieldPermission != null) {
            return fieldPermission.expression();
        }
        if (classPermission != null) {
            return classPermission.expression();
        }
        return null;
    }

    /**
     * Get the calculated {@link UpdatePermission} value for the field.
     *
     * @param clazz the entity class
     * @param fieldName the field
     * @return the update permissions for a field
     */
    protected String getUpdatePermission(Type clazz, String fieldName) {
        UpdatePermission classPermission = dictionary.getAnnotation(clazz, UpdatePermission.class);
        UpdatePermission fieldPermission = dictionary.getAttributeOrRelationAnnotation(clazz, UpdatePermission.class,
                fieldName);

        if (fieldPermission != null) {
            return fieldPermission.expression();
        }
        if (classPermission != null) {
            return classPermission.expression();
        }
        return null;
    }

    /**
     * Get the calculated {@link CreatePermission} value for the field.
     *
     * @param clazz the entity class
     * @return the create permissions for an entity
     */
    protected String getCreatePermission(Type clazz) {
        CreatePermission classPermission = dictionary.getAnnotation(clazz, CreatePermission.class);

        if (classPermission != null) {
            return classPermission.expression();
        }
        return null;
    }

    /**
     * Get the calculated {@link DeletePermission} value for the field.
     *
     * @param clazz the entity class
     * @return the delete permissions for an entity
     */
    protected String getDeletePermission(Type clazz) {
        DeletePermission classPermission = dictionary.getAnnotation(clazz, DeletePermission.class);

        if (classPermission != null) {
            return classPermission.expression();
        }
        return null;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy