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

de.escalon.hypermedia.spring.siren.SirenUtils Maven / Gradle / Ivy

There is a newer version: 0.4.2
Show newest version
package de.escalon.hypermedia.spring.siren;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.node.ObjectNode;
import de.escalon.hypermedia.PropertyUtils;
import de.escalon.hypermedia.action.Type;
import de.escalon.hypermedia.affordance.ActionDescriptor;
import de.escalon.hypermedia.affordance.ActionInputParameter;
import de.escalon.hypermedia.affordance.Affordance;
import de.escalon.hypermedia.affordance.DataType;
import de.escalon.hypermedia.spring.DefaultDocumentationProvider;
import de.escalon.hypermedia.spring.DocumentationProvider;
import de.escalon.hypermedia.spring.SpringActionInputParameter;
import org.springframework.core.MethodParameter;
import org.springframework.hateoas.*;
import org.springframework.hateoas.core.DefaultRelProvider;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

/**
 * Maps spring-hateoas response data to siren data. Created by Dietrich on 17.04.2016.
 */
public class SirenUtils {

    private static final Set FILTER_RESOURCE_SUPPORT = new HashSet(Arrays.asList("class", "links",
            "id"));
    private String requestMediaType;

    private Set navigationalRels = new HashSet(Arrays.asList("self", "next", "previous", "prev"));

    private RelProvider relProvider = new DefaultRelProvider();

    private DocumentationProvider documentationProvider = new DefaultDocumentationProvider();

    public void toSirenEntity(SirenEntityContainer objectNode, Object object) {
        if (object == null) {
            return;
        }
        try {
            if (object instanceof Resource) {
                Resource resource = (Resource) object;
                objectNode.setLinks(this.toSirenLinks(
                        getNavigationalLinks(resource.getLinks())));
                objectNode.setEmbeddedLinks(this.toSirenEmbeddedLinks(
                        getEmbeddedLinks(resource.getLinks())));
                objectNode.setActions(this.toSirenActions(getActions(resource.getLinks())));
                toSirenEntity(objectNode, resource.getContent());
                return;
            } else if (object instanceof Resources) {
                Resources resources = (Resources) object;

                objectNode.setLinks(this.toSirenLinks(getNavigationalLinks(resources.getLinks())));
                Collection content = resources.getContent();
                toSirenEntity(objectNode, content);
                objectNode.setActions(this.toSirenActions(getActions(resources.getLinks())));
                return;
            } else if (object instanceof ResourceSupport) {
                ResourceSupport resource = (ResourceSupport) object;
                objectNode.setLinks(this.toSirenLinks(
                        getNavigationalLinks(resource.getLinks())));
                objectNode.setEmbeddedLinks(this.toSirenEmbeddedLinks(
                        getEmbeddedLinks(resource.getLinks())));
                objectNode.setActions(this.toSirenActions(
                        getActions(resource.getLinks())));

                // wrap object attributes below to avoid endless loop

            } else if (object instanceof Collection) {
                Collection collection = (Collection) object;
                for (Object item : collection) {
                    SirenEmbeddedRepresentation child = new SirenEmbeddedRepresentation();
                    toSirenEntity(child, item);
                    objectNode.addSubEntity(child);
                }
                return;
            }
            if (object instanceof Map) {
                Map map = (Map) object;
                Map propertiesNode = new HashMap();
                objectNode.setProperties(propertiesNode);
                for (Map.Entry entry : map.entrySet()) {
                    String key = entry.getKey()
                            .toString();
                    Object content = entry.getValue();

                    String docUrl = documentationProvider.getDocumentationUrl(key, content);
                    traverseAttribute(objectNode, propertiesNode, key, docUrl, content);
                }
            } else { // bean or ResourceSupport
                objectNode.setSirenClasses(getSirenClasses(object));
                Map propertiesNode = new HashMap();
                createRecursiveSirenEntitiesFromPropertiesAndFields(objectNode, propertiesNode, object);
                objectNode.setProperties(propertiesNode);
            }
        } catch (Exception ex) {
            throw new RuntimeException("failed to transform object " + object, ex);
        }
    }

    private List getSirenClasses(Object object) {
        List sirenClasses;
        String sirenClass = relProvider.getItemResourceRelFor(object.getClass());
        if (sirenClass != null) {
            sirenClasses = Collections.singletonList(sirenClass);
        } else {
            sirenClasses = Collections.emptyList();
        }
        return sirenClasses;
    }

    private List getEmbeddedLinks(List links) {
        List ret = new ArrayList();
        for (Link link : links) {
            if (!navigationalRels.contains(link.getRel())) {
                if (link instanceof Affordance) {
                    Affordance affordance = (Affordance) link;
                    List actionDescriptors = affordance.getActionDescriptors();
                    for (ActionDescriptor actionDescriptor : actionDescriptors) {
                        if ("GET".equals(actionDescriptor.getHttpMethod()) && !affordance.isTemplated()) {
                            ret.add(link);
                        }
                    }
                } else {
                    // templated links are actions, not embedded links
                    if(!link.isTemplated()) {
                        ret.add(link);
                    }
                }
            }
        }
        return ret;
    }

    private List getNavigationalLinks(List links) {
        List ret = new ArrayList();
        for (Link link : links) {
            if (navigationalRels.contains(link.getRel())) {
                ret.add(link);
            }
        }
        return ret;
    }

    private List getActions(List links) {
        List ret = new ArrayList();
        for (Link link : links) {
            if (link instanceof Affordance) {
                Affordance affordance = (Affordance) link;

                List actionDescriptors = affordance.getActionDescriptors();
                for (ActionDescriptor actionDescriptor : actionDescriptors) {
                    // non-self GET non-GET and templated links are actions
                    if (!("GET".equals(actionDescriptor.getHttpMethod())) || affordance.isTemplated()) {
                        ret.add(link);
                        // add just once for eligible link
                        break;
                    }
                }
            } else {
                // templated links are actions
                if (!navigationalRels.contains(link.getRel()) && link.isTemplated()) {
                    ret.add(link);
                }
            }
        }
        return ret;
    }


    private void createRecursiveSirenEntitiesFromPropertiesAndFields(SirenEntityContainer objectNode, Map propertiesNode,
                                                                     Object object) throws InvocationTargetException,
            IllegalAccessException {
        Map propertyDescriptors = PropertyUtils.getPropertyDescriptors(object);
        for (PropertyDescriptor propertyDescriptor : propertyDescriptors.values()) {
            String name = propertyDescriptor.getName();
            if (FILTER_RESOURCE_SUPPORT.contains(name)) {
                continue;
            }

            Method readMethod = propertyDescriptor.getReadMethod();
            if (readMethod != null) {
                Object content = readMethod
                        .invoke(object);
                String docUrl = documentationProvider.getDocumentationUrl(readMethod, content);
                traverseAttribute(objectNode, propertiesNode, name, docUrl, content);
            }
        }

        Field[] fields = object.getClass()
                .getFields();
        for (Field field : fields) {
            String name = field.getName();
            if (!propertyDescriptors.containsKey(name)) {
                Object content = field.get(object);
                String docUrl = documentationProvider.getDocumentationUrl(field, content);
                traverseAttribute(objectNode, propertiesNode, name, docUrl, content);
            }
        }
    }

    private void traverseAttribute(SirenEntityContainer objectNode, Map propertiesNode,
                                   String name, String docUrl, Object content) throws
            InvocationTargetException, IllegalAccessException {
        Object value = getContentAsScalarValue(content);

        if (value != NULL_VALUE) {
            if (value != null) {
                // for each scalar property of a simple bean, add valuepair
                propertiesNode.put(name, value);
            } else {
                if (content instanceof Resources) {
                    toSirenEntity(objectNode, content);
                } else if (content instanceof ResourceSupport) {
                    traverseSingleSubEntity(objectNode, content, name, docUrl);
                } else if (content instanceof Collection) {
                    Collection collection = (Collection) content;
                    for (Object item : collection) {
                        if (DataType.isSingleValueType(item.getClass())) {
                            Object listObject = propertiesNode.get(name);
                            if (listObject == null) {
                                listObject = new ArrayList();
                                propertiesNode.put(name, listObject);
                            }
                            if (listObject instanceof Collection) {
                                ((Collection) listObject).add(item);
                            }
                        } else if (item != null) {
                            traverseSingleSubEntity(objectNode, item, name, docUrl);
                        }
                    }
                } else if (content instanceof Map) {
                    Set> entries = ((Map) content).entrySet();
                    Map subProperties = new HashMap();
                    propertiesNode.put(name, subProperties);
                    for (Map.Entry entry : entries) {
                        traverseAttribute(objectNode, subProperties, entry.getKey(), docUrl, entry.getValue());
                    }
                } else {
                    Map nestedProperties = new HashMap();
                    propertiesNode.put(name, nestedProperties);
                    createRecursiveSirenEntitiesFromPropertiesAndFields(objectNode, nestedProperties, content);
                }
            }
        }
    }

    private void traverseSingleSubEntity(SirenEntityContainer objectNode, Object content,
                                         String name, String docUrl)
            throws InvocationTargetException, IllegalAccessException {

        Object bean;
        List links;
        if (content instanceof Resource) {
            bean = ((Resource) content).getContent();
            links = ((Resource) content).getLinks();
        } else if (content instanceof ResourceSupport) {
            bean = content;
            links = ((ResourceSupport) content).getLinks();
        } else {
            bean = content;
            links = Collections.emptyList();
        }

        Map properties = new HashMap();
        List rels = Collections.singletonList(docUrl != null ? docUrl : name);
        SirenEmbeddedRepresentation subEntity = new SirenEmbeddedRepresentation(
                getSirenClasses(bean), properties, null, toSirenActions(getActions(links)),
                toSirenLinks(getNavigationalLinks(links)), rels, null);
        //subEntity.setProperties(properties);
        objectNode.addSubEntity(subEntity);
        List sirenEmbeddedLinks = toSirenEmbeddedLinks(getEmbeddedLinks(links));
        for (SirenEmbeddedLink sirenEmbeddedLink : sirenEmbeddedLinks) {
            subEntity.addSubEntity(sirenEmbeddedLink);
        }
        createRecursiveSirenEntitiesFromPropertiesAndFields(subEntity, properties, bean);
    }

    private List toSirenActions(List links) {
        List ret = new ArrayList();
        for (Link link : links) {
            if (link instanceof Affordance) {
                Affordance affordance = (Affordance) link;
                List actionDescriptors = affordance.getActionDescriptors();
                for (ActionDescriptor actionDescriptor : actionDescriptors) {
                    List fields = toSirenFields(actionDescriptor);
                    // TODO integrate getActions and this method so we do not need this check:
                    // only templated affordances or non-get affordances are actions
                    if (!"GET".equals(actionDescriptor.getHttpMethod()) || affordance.isTemplated()) {
                        String href;
                        if (affordance.isTemplated()) {
                            href = affordance.getUriTemplateComponents()
                                    .getBaseUri();
                        } else {
                            href = affordance.getHref();
                        }

                        SirenAction sirenAction = new SirenAction(null, actionDescriptor.getActionName(), null,
                                actionDescriptor.getHttpMethod(), href, requestMediaType, fields);
                        ret.add(sirenAction);
                    }
                }
            } else if (link.isTemplated()) {
                List fields = new ArrayList();
                List variables = link.getVariables();
                boolean queryOnly = false;
                for (TemplateVariable variable : variables) {
                    queryOnly = isQueryParam(variable);
                    if (!queryOnly) {
                        break;
                    }
                    fields.add(new SirenField(variable.getName(), "text", (String) null, variable.getDescription(),
                            null));
                }
                // no support for non-query fields in siren
                if (queryOnly) {
                    String baseUri = new UriTemplate(link.getHref()).expand()
                            .toASCIIString();
                    SirenAction sirenAction = new SirenAction(null, null, null, "GET",
                            baseUri, null, fields);
                    ret.add(sirenAction);
                }
            }
        }
        return ret;
    }

    private boolean isQueryParam(TemplateVariable variable) {
        boolean queryOnly;
        switch (variable.getType()) {
            case REQUEST_PARAM:
            case REQUEST_PARAM_CONTINUED:
                queryOnly = true;
                break;
            default:
                queryOnly = false;
        }
        return queryOnly;
    }

    private List toSirenFields(ActionDescriptor actionDescriptor) {
        List ret = new ArrayList();
        if (actionDescriptor.hasRequestBody()) {
            recurseBeanCreationParams(ret, actionDescriptor.getRequestBody()
                    .getParameterType(), actionDescriptor, actionDescriptor.getRequestBody(), actionDescriptor
                    .getRequestBody()
                    .getValue(), "", Collections.emptySet());
        } else {
            Collection paramNames = actionDescriptor.getRequestParamNames();
            for (String paramName : paramNames) {
                ActionInputParameter inputParameter = actionDescriptor.getActionInputParameter(paramName);
                Object[] possibleValues = inputParameter.getPossibleValues(actionDescriptor);

                ret.add(createSirenField(paramName, inputParameter.getValueFormatted(), inputParameter,
                        possibleValues));
            }
        }
        return ret;
    }

    /**
     * Renders input fields for bean properties of bean to add or update or patch.
     *
     * @param sirenFields to add to
     * @param beanType to render
     * @param annotatedParameters which describes the method
     * @param annotatedParameter which requires the bean
     * @param currentCallValue sample call value
     */
    private void recurseBeanCreationParams(List sirenFields, Class beanType,
                                           ActionDescriptor annotatedParameters,
                                           ActionInputParameter annotatedParameter, Object currentCallValue,
                                           String parentParamName, Set knownFields) {
        // TODO collection, map and object node creation are only describable by an annotation, not via type reflection
        if (ObjectNode.class.isAssignableFrom(beanType) || Map.class.isAssignableFrom(beanType)
                || Collection.class.isAssignableFrom(beanType) || beanType.isArray()) {
            return; // use @Input(include) to list parameter names, at least? Or mix with hdiv's form builder?
        }
        try {
            Constructor[] constructors = beanType.getConstructors();
            // find default ctor
            Constructor constructor = PropertyUtils.findDefaultCtor(constructors);
            // find ctor with JsonCreator ann
            if (constructor == null) {
                constructor = PropertyUtils.findJsonCreator(constructors, JsonCreator.class);
            }
            Assert.notNull(constructor, "no default constructor or JsonCreator found for type " + beanType
                    .getName());
            int parameterCount = constructor.getParameterTypes().length;

            if (parameterCount > 0) {
                Annotation[][] annotationsOnParameters = constructor.getParameterAnnotations();

                Class[] parameters = constructor.getParameterTypes();
                int paramIndex = 0;
                for (Annotation[] annotationsOnParameter : annotationsOnParameters) {
                    for (Annotation annotation : annotationsOnParameter) {
                        if (JsonProperty.class == annotation.annotationType()) {
                            JsonProperty jsonProperty = (JsonProperty) annotation;

                            // TODO use required attribute of JsonProperty for required fields
                            String paramName = jsonProperty.value();
                            Class parameterType = parameters[paramIndex];
                            Object propertyValue = PropertyUtils.getPropertyOrFieldValue(currentCallValue,
                                    paramName);
                            MethodParameter methodParameter = new MethodParameter(constructor, paramIndex);

                            addSirenFieldsForMethodParameter(sirenFields, methodParameter, annotatedParameter,
                                    annotatedParameters,
                                    parentParamName, paramName, parameterType, propertyValue,
                                    knownFields);
                            paramIndex++; // increase for each @JsonProperty
                        }
                    }
                }
                Assert.isTrue(parameters.length == paramIndex,
                        "not all constructor arguments of @JsonCreator " + constructor.getName() +
                                " are annotated with @JsonProperty");
            }

            Set knownConstructorFields = new HashSet(sirenFields.size());
            for (SirenField sirenField : sirenFields) {
                knownConstructorFields.add(sirenField.getName());
            }

            // TODO support Option provider by other method args?
            Map propertyDescriptors = PropertyUtils.getPropertyDescriptors(beanType);

            // add input field for every setter
            for (PropertyDescriptor propertyDescriptor : propertyDescriptors.values()) {
                final Method writeMethod = propertyDescriptor.getWriteMethod();
                String propertyName = propertyDescriptor.getName();

                if (writeMethod == null || knownFields.contains(parentParamName + propertyName)) {
                    continue;
                }
                final Class propertyType = propertyDescriptor.getPropertyType();

                Object propertyValue = PropertyUtils.getPropertyOrFieldValue(currentCallValue, propertyName);
                MethodParameter methodParameter = new MethodParameter(propertyDescriptor.getWriteMethod(), 0);

                addSirenFieldsForMethodParameter(sirenFields, methodParameter, annotatedParameter,
                        annotatedParameters,
                        parentParamName, propertyName, propertyType, propertyValue, knownConstructorFields);
            }
        } catch (Exception e) {
            throw new RuntimeException("Failed to write input fields for constructor", e);
        }
    }

    private void addSirenFieldsForMethodParameter(List sirenFields, MethodParameter
            methodParameter, ActionInputParameter annotatedParameter, ActionDescriptor annotatedParameters, String
                                                          parentParamName, String paramName, Class
                                                          parameterType, Object propertyValue, Set
                                                          knownFields) {
        if (DataType.isSingleValueType(parameterType)
                || DataType.isArrayOrCollection(parameterType)) {

            if (annotatedParameter.isIncluded(paramName) && !knownFields.contains(parentParamName + paramName)) {

                ActionInputParameter constructorParamInputParameter =
                        new SpringActionInputParameter(methodParameter, propertyValue);

                final Object[] possibleValues =
                        annotatedParameter.getPossibleValues(methodParameter, annotatedParameters);

                // dot-separated property path as field name
                SirenField sirenField = createSirenField(parentParamName + paramName,
                        propertyValue, constructorParamInputParameter, possibleValues);
                sirenFields.add(sirenField);
            }
        } else {
            Object callValueBean;
            if (propertyValue instanceof Resource) {
                callValueBean = ((Resource) propertyValue).getContent();
            } else {
                callValueBean = propertyValue;
            }
            recurseBeanCreationParams(sirenFields, parameterType, annotatedParameters,
                    annotatedParameter,
                    callValueBean, paramName + ".", knownFields);
        }
    }

    private SirenField createSirenField(String paramName, Object propertyValue,
                                        ActionInputParameter inputParameter, Object[] possibleValues) {
        SirenField sirenField;
        if (possibleValues.length == 0) {
            String propertyValueAsString = propertyValue == null ? null : propertyValue
                    .toString();
            Type htmlInputFieldType = inputParameter.getHtmlInputFieldType();
            // TODO: null -> array or bean parameter without possible values
            String type = htmlInputFieldType == null ? "text" :
                    htmlInputFieldType
                            .name()
                            .toLowerCase();
            sirenField = new SirenField(paramName,
                    type,
                    propertyValueAsString, null, null);
        } else {
            List sirenPossibleValues = new ArrayList();
            String type;
            if (inputParameter.isArrayOrCollection()) {
                type = "checkbox";
                for (Object possibleValue : possibleValues) {
                    boolean selected = ObjectUtils.containsElement(
                            inputParameter.getValues(),
                            possibleValue);
                    // TODO have more useful value title
                    sirenPossibleValues.add(new SirenFieldValue(possibleValue.toString(), possibleValue, selected));
                }
            } else {
                type = "radio";
                for (Object possibleValue : possibleValues) {
                    boolean selected = possibleValue.equals(propertyValue);
                    sirenPossibleValues.add(new SirenFieldValue(possibleValue.toString(), possibleValue, selected));
                }
            }
            sirenField = new SirenField(paramName,
                    type,
                    sirenPossibleValues, null, null);
        }
        return sirenField;
    }


    private List toSirenLinks(List links) {
        List ret = new ArrayList();
        for (Link link : links) {
            if (link instanceof Affordance) {
                ret.add(new SirenLink(null, ((Affordance) link).getRels(), link.getHref(), null, null));
            } else {
                ret.add(new SirenLink(null, Collections.singletonList(link.getRel()), link.getHref(), null, null));
            }
        }
        return ret;
    }

    private List toSirenEmbeddedLinks(List links) {
        List ret = new ArrayList();
        for (Link link : links) {
            if (link instanceof Affordance) {
                // TODO: how to determine classes? type of target resource? collection/item?
                ret.add(new SirenEmbeddedLink(null, ((Affordance) link).getRels(), link
                        .getHref(), null, null));
            } else {
                ret.add(new SirenEmbeddedLink(null, Collections.singletonList(link.getRel()), link
                        .getHref(), null, null));
            }
        }
        return ret;
    }


    static class NullValue {

    }

    public static final NullValue NULL_VALUE = new NullValue();

    private Object getContentAsScalarValue(Object content) {
        Object value = null;

        if (content == null) {
            value = NULL_VALUE;
        } else if (DataType.isSingleValueType(content.getClass())) {
            value = DataType.asScalarValue(content);
        }
        return value;
    }

    public void setRequestMediaType(String requestMediaType) {
        this.requestMediaType = requestMediaType;
    }

    public void setRelProvider(RelProvider relProvider) {
        this.relProvider = relProvider;
    }

    public void setDocumentationProvider(DocumentationProvider documentationProvider) {
        this.documentationProvider = documentationProvider;
    }

    public void setAdditionalNavigationalRels(Collection additionalNavigationalRels) {
        this.navigationalRels.addAll(additionalNavigationalRels);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy