de.escalon.hypermedia.spring.uber.UberUtils.java Source code

Java tutorial

Introduction

Here is the source code for de.escalon.hypermedia.spring.uber.UberUtils.java

Source

/*
 * 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<String> FILTER_RESOURCE_SUPPORT = new HashSet<String>(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<String> 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<String, PropertyDescriptor> 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<String> rels) {
        Assert.notNull(actionDescriptor, "actionDescriptor must not be null");
        UberNode uberLink = new UberNode();
        uberLink.setRel(rels);
        PartialUriTemplateComponents partialUriTemplateComponents = new PartialUriTemplate(href)
                .expand(Collections.<String, Object>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<UberField> uberFields = new ArrayList<UberField>();
            recurseBeanCreationParams(uberFields, actionDescriptor.getRequestBody().getParameterType(),
                    actionDescriptor, actionDescriptor.getRequestBody(),
                    actionDescriptor.getRequestBody().getValue(), "", Collections.<String>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<SirenAction> toUberActions(List<Link> links) {
    //        List<SirenAction> ret = new ArrayList<SirenAction>();
    //        for (Link link : links) {
    //            if (link instanceof Affordance) {
    //                Affordance affordance = (Affordance) link;
    //                List<ActionDescriptor> actionDescriptors = affordance.getActionDescriptors();
    //                for (ActionDescriptor actionDescriptor : actionDescriptors) {
    //                    List<SirenField> 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<SirenField> fields = new ArrayList<SirenField>();
    //                List<TemplateVariable> 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<SirenField> toUberFields(ActionDescriptor actionDescriptor) {
    //        List<SirenField> ret = new ArrayList<SirenField>();
    //        if (actionDescriptor.hasRequestBody()) {
    //            recurseBeanCreationParams(ret, actionDescriptor.getRequestBody()
    //                    .getParameterType(), actionDescriptor, actionDescriptor.getRequestBody(), actionDescriptor
    //                    .getRequestBody()
    //                    .getValue(), "", Collections.<String>emptySet());
    //        }
    ////        } else {
    ////            Collection<String> 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<UberField> uberFields, Class<?> beanType,
            ActionDescriptor annotatedParameters, ActionInputParameter annotatedParameter, Object currentCallValue,
            String parentParamName, Set<String> 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<String> knownConstructorFields = new HashSet<String>(uberFields.size());
            for (UberField sirenField : uberFields) {
                knownConstructorFields.add(sirenField.getName());
            }

            // TODO support Option provider by other method args?
            Map<String, PropertyDescriptor> 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<ActionDescriptor> getActionDescriptors(Link link) {
        List<ActionDescriptor> 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<String> getRels(Link link) {
        List<String> rels;
        if (link instanceof Affordance) {
            rels = ((Affordance) link).getRels();
        } else {
            rels = Arrays.asList(link.getRel());
        }
        return rels;
    }

    private static void addUberFieldsForMethodParameter(List<UberField> fields, MethodParameter methodParameter,
            ActionInputParameter annotatedParameter, ActionDescriptor annotatedParameters, String parentParamName,
            String paramName, Class parameterType, Object propertyValue, Set<String> 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<SirenFieldValue> sirenPossibleValues = new ArrayList<SirenFieldValue>();
        //            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;
    }

}