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

de.escalon.hypermedia.spring.uber.UberUtils Maven / Gradle / Ivy

There is a newer version: 0.4.2
Show newest version
/*
 * Copyright (c) 2015. Escalon System-Entwicklung, Dietrich Schulten
 *
 * 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
 *
 * http://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 de.escalon.hypermedia.spring.uber;

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.*;
import de.escalon.hypermedia.spring.SpringActionDescriptor;
import de.escalon.hypermedia.spring.SpringActionInputParameter;
import org.springframework.core.MethodParameter;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMethod;

import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.Map.Entry;

public class UberUtils {

    private UberUtils() {

    }

    static final Set FILTER_RESOURCE_SUPPORT = new HashSet(Arrays.asList("class", "links", "id"));
    static final String MODEL_FORMAT = "%s={%s}";


    /**
     * Recursively converts object to nodes of uber data.
     *
     * @param objectNode
     *         to append to
     * @param object
     *         to convert
     */
    public static void toUberData(AbstractUberNode objectNode, Object object) {
        Set filtered = FILTER_RESOURCE_SUPPORT;
        if (object == null) {
            return;
        }

        try {
            // TODO: move all returns to else branch of property descriptor handling
            if (object instanceof Resource) {
                Resource resource = (Resource) object;
                objectNode.addLinks(resource.getLinks());
                toUberData(objectNode, resource.getContent());
                return;
            } else if (object instanceof Resources) {
                Resources resources = (Resources) object;

                // TODO set name using EVO see HypermediaSupportBeanDefinitionRegistrar

                objectNode.addLinks(resources.getLinks());

                Collection content = resources.getContent();
                toUberData(objectNode, content);
                return;
            } else if (object instanceof ResourceSupport) {
                ResourceSupport resource = (ResourceSupport) object;

                objectNode.addLinks(resource.getLinks());

                // wrap object attributes below to avoid endless loop

            } else if (object instanceof Collection) {
                Collection collection = (Collection) object;
                for (Object item : collection) {
                    // TODO name must be repeated for each collection item
                    UberNode itemNode = new UberNode();
                    objectNode.addData(itemNode);
                    toUberData(itemNode, item);
                }
                return;
            }
            if (object instanceof Map) {
                Map map = (Map) object;
                for (Entry entry : map.entrySet()) {
                    String key = entry.getKey()
                            .toString();
                    Object content = entry.getValue();
                    Object value = getContentAsScalarValue(content);
                    UberNode entryNode = new UberNode();
                    objectNode.addData(entryNode);
                    entryNode.setName(key);
                    if (value != null) {
                        entryNode.setValue(value);
                    } else {
                        toUberData(entryNode, content);
                    }
                }
            } else {
                Map propertyDescriptors = PropertyUtils.getPropertyDescriptors(object);
                for (PropertyDescriptor propertyDescriptor : propertyDescriptors.values()) {
                    String name = propertyDescriptor.getName();
                    if (filtered.contains(name)) {
                        continue;
                    }
                    UberNode propertyNode = new UberNode();
                    Object content = propertyDescriptor.getReadMethod()
                            .invoke(object);

                    if (isEmptyCollectionOrMap(content, propertyDescriptor.getPropertyType())) {
                        continue;
                    }

                    Object value = getContentAsScalarValue(content);
                    propertyNode.setName(name);
                    objectNode.addData(propertyNode);
                    if (value != null) {
                        // for each scalar property of a simple bean, add valuepair nodes to data
                        propertyNode.setValue(value);
                    } else {
                        toUberData(propertyNode, content);
                    }
                }

                Field[] fields = object.getClass()
                        .getFields();
                for (Field field : fields) {
                    String name = field.getName();
                    if (!propertyDescriptors.containsKey(name)) {
                        Object content = field.get(object);
                        Class type = field.getType();
                        if (isEmptyCollectionOrMap(content, type)) {
                            continue;
                        }
                        UberNode propertyNode = new UberNode();

                        Object value = getContentAsScalarValue(content);
                        propertyNode.setName(name);
                        objectNode.addData(propertyNode);
                        if (value != null) {
                            // for each scalar property of a simple bean, add valuepair nodes to data
                            propertyNode.setValue(value);
                        } else {
                            toUberData(propertyNode, content);
                        }

                    }
                }
            }
        } catch (Exception ex) {
            throw new RuntimeException("failed to transform object " + object, ex);
        }
    }

    private static boolean isEmptyCollectionOrMap(Object content, Class type) {
        if (Collection.class.isAssignableFrom(type)) {
            if (content == null) {
                return true;
            } else {
                if (((List) content).isEmpty()) {
                    return true;
                }
            }
        } else if (Map.class.isAssignableFrom(type)) {
            if (content == null) {
                return true;
            } else {
                if (((List) content).isEmpty()) {
                    return true;
                }
            }
        }
        return false;
    }


    private static Object getContentAsScalarValue(Object content) {
        final Object value;
        if (content == null) {
            value = UberNode.NULL_VALUE;
        } else if (DataType.isSingleValueType(content.getClass())) {
            value = content.toString();
        } else {
            value = null;
        }
        return value;
    }

    /**
     * Converts single link to uber node.
     *
     * @param href
     *         to use
     * @param actionDescriptor
     *         to use for action and model, never null
     * @param rels
     *         of the link
     * @return uber link
     */
    public static UberNode toUberLink(String href, ActionDescriptor actionDescriptor, String... rels) {
        return toUberLink(href, actionDescriptor, Arrays.asList(rels));
    }

    /**
     * Converts single link to uber node.
     *
     * @param href
     *         to use
     * @param actionDescriptor
     *         to use for action and model, never null
     * @param rels
     *         of the link
     * @return uber link
     */
    public static UberNode toUberLink(String href, ActionDescriptor actionDescriptor, List rels) {
        Assert.notNull(actionDescriptor, "actionDescriptor must not be null");
        UberNode uberLink = new UberNode();
        uberLink.setRel(rels);
        PartialUriTemplateComponents partialUriTemplateComponents = new PartialUriTemplate(href).expand(Collections
                .emptyMap());
        uberLink.setUrl(partialUriTemplateComponents.toString());
        uberLink.setTemplated(partialUriTemplateComponents.hasVariables() ? Boolean.TRUE : null);
        uberLink.setModel(getModelProperty(href, actionDescriptor));
        if (actionDescriptor != null) {
            RequestMethod requestMethod = RequestMethod.valueOf(actionDescriptor.getHttpMethod());
            uberLink.setAction(UberAction.forRequestMethod(requestMethod));
        }
        return uberLink;
    }

    private static String getModelProperty(String href, ActionDescriptor actionDescriptor) {

        RequestMethod httpMethod = RequestMethod.valueOf(actionDescriptor.getHttpMethod());
        StringBuffer model = new StringBuffer();

        switch (httpMethod) {
            case POST:
            case PUT:
            case PATCH: {
                List uberFields = new ArrayList();
                recurseBeanCreationParams(uberFields, actionDescriptor.getRequestBody()
                        .getParameterType(), actionDescriptor, actionDescriptor.getRequestBody(), actionDescriptor
                        .getRequestBody()
                        .getValue(), "", Collections.emptySet());
                for (UberField uberField : uberFields) {
                    if (model.length() > 0) {
                        model.append("&");
                    }
                    model.append(String.format(MODEL_FORMAT, uberField.getName(), uberField.getName()));
                }
                break;
            }
            default:

        }
        return model.length() == 0 ? null : model.toString();
    }


//    private List toUberActions(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 = toUberFields(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 List toUberFields(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 uberFields
     *         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 static void recurseBeanCreationParams(List uberFields, 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);

                            addUberFieldsForMethodParameter(uberFields, 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(uberFields.size());
            for (UberField sirenField : uberFields) {
                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);

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

    public static List getActionDescriptors(Link link) {
        List actionDescriptors;
        if (link instanceof Affordance) {
            actionDescriptors = ((Affordance) link).getActionDescriptors();
        } else {
            SpringActionDescriptor actionDescriptor = new SpringActionDescriptor("get", RequestMethod.GET
                    .name());
            PartialUriTemplate partialUriTemplate = new PartialUriTemplate(link.getHref());
            PartialUriTemplateComponents parts = partialUriTemplate.asComponents();
            actionDescriptors = Arrays.asList((ActionDescriptor) actionDescriptor);
        }
        return actionDescriptors;
    }

    public static List getRels(Link link) {
        List rels;
        if (link instanceof Affordance) {
            rels = ((Affordance) link).getRels();
        } else {
            rels = Arrays.asList(link.getRel());
        }
        return rels;
    }

    private static void addUberFieldsForMethodParameter(List fields, 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
                UberField field = createUberField(parentParamName + paramName,
                        propertyValue, constructorParamInputParameter, possibleValues);
                fields.add(field);
            }
        } else {
            Object callValueBean;
            if (propertyValue instanceof Resource) {
                callValueBean = ((Resource) propertyValue).getContent();
            } else {
                callValueBean = propertyValue;
            }
            recurseBeanCreationParams(fields, parameterType, annotatedParameters,
                    annotatedParameter,
                    callValueBean, paramName + ".", knownFields);
        }
    }

    private static UberField createUberField(String paramName, Object propertyValue,
                                             ActionInputParameter inputParameter, Object[] possibleValues) {
        UberField field;
//        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();
        field = new UberField(paramName,
                propertyValueAsString);
//        } 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));
//                }
//            }
//            field = new UberField(paramName,
//                    sirenPossibleValues);
//    }

        return field;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy