de.escalon.hypermedia.spring.SpringActionDescriptor.java Source code

Java tutorial

Introduction

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

Source

/*
 * Copyright (c) 2014. 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;

import de.escalon.hypermedia.action.Action;
import de.escalon.hypermedia.action.Cardinality;
import de.escalon.hypermedia.affordance.ActionDescriptor;
import de.escalon.hypermedia.affordance.ActionInputParameter;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.beans.PropertyAccessorUtils;
import org.springframework.core.MethodParameter;
import org.springframework.util.Assert;

import java.beans.PropertyDescriptor;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Describes an HTTP method independently of a specific rest framework. Has knowledge about possible request data, i.e.
 * which types and values are suitable for an action. For example, an action descriptor can be used to create a form
 * with select options and typed input fields that calls a POST handler. It has {@link ActionInputParameter}s which
 * represent method handler arguments. Supported method handler arguments are: <ul> <li>path variables</li> <li>request
 * params (url query params)</li> <li>request headers</li> <li>request body</li> </ul>
 *
 * @author Dietrich Schulten
 */
public class SpringActionDescriptor implements ActionDescriptor {

    private String httpMethod;
    private String actionName;

    private String semanticActionType;
    private Map<String, ActionInputParameter> requestParams = new LinkedHashMap<String, ActionInputParameter>();
    private Map<String, ActionInputParameter> pathVariables = new LinkedHashMap<String, ActionInputParameter>();
    private Map<String, ActionInputParameter> requestHeaders = new LinkedHashMap<String, ActionInputParameter>();
    private Map<String, ActionInputParameter> inputParams = new LinkedHashMap<String, ActionInputParameter>();

    private ActionInputParameter requestBody;
    private Cardinality cardinality = Cardinality.SINGLE;

    /**
     * Creates an {@link ActionDescriptor}.
     *
     * @param actionName
     *         name of the action, e.g. the method name of the handler method. Can be used by an action representation,
     *         e.g. to identify the action using a form name.
     * @param httpMethod
     *         used during submit
     */
    public SpringActionDescriptor(String actionName, String httpMethod) {
        Assert.notNull(actionName);
        Assert.notNull(httpMethod);
        this.httpMethod = httpMethod;
        this.actionName = actionName;
    }

    /**
     * The name of the action, for use as form name, usually the method name of the handler method.
     *
     * @return action name, never null
     */
    @Override
    public String getActionName() {
        return actionName;
    }

    /**
     * Gets the http method of this action.
     *
     * @return method, never null
     */
    @Override
    public String getHttpMethod() {
        return httpMethod;
    }

    /**
     * Gets the path variable names.
     *
     * @return names or empty collection, never null
     */
    @Override
    public Collection<String> getPathVariableNames() {
        return pathVariables.keySet();
    }

    /**
     * Gets the request header names.
     *
     * @return names or empty collection, never null
     */
    @Override
    public Collection<String> getRequestHeaderNames() {
        return requestHeaders.keySet();
    }

    /**
     * Gets the request parameter (query param) names.
     *
     * @return names or empty collection, never null
     */
    @Override
    public Collection<String> getRequestParamNames() {
        return requestParams.keySet();
    }

    /**
     * Adds descriptor for request param.
     *
     * @param key
     *         name of request param
     * @param actionInputParameter
     *         descriptor
     */
    public void addRequestParam(String key, ActionInputParameter actionInputParameter) {
        requestParams.put(key, actionInputParameter);
    }

    /**
     * Adds descriptor for params annotated with <code>@Input</code> which are not also annotated as
     * <code>@RequestParam</code>, <code>@PathVariable</code>, <code>@RequestBody</code> or
     * <code>@RequestHeader</code>.
     * Input parameter beans or maps are filled from query params by Spring, and this allows to describe them with
     * UriTemplates.
     *
     * @param key
     *         name of request param
     * @param actionInputParameter
     *         descriptor
     */
    public void addInputParam(String key, ActionInputParameter actionInputParameter) {
        inputParams.put(key, actionInputParameter);
    }

    /**
     * Adds descriptor for path variable.
     *
     * @param key
     *         name of path variable
     * @param actionInputParameter
     *         descriptorg+ann#2
     */

    public void addPathVariable(String key, ActionInputParameter actionInputParameter) {
        pathVariables.put(key, actionInputParameter);
    }

    /**
     * Adds descriptor for request header.
     *
     * @param key
     *         name of request header
     * @param actionInputParameter
     *         descriptor
     */
    public void addRequestHeader(String key, ActionInputParameter actionInputParameter) {
        requestHeaders.put(key, actionInputParameter);
    }

    /**
     * Gets input parameter info which is part of the URL mapping, be it request parameters, path variables or request
     * body attributes.
     *
     * @param name
     *         to retrieve
     * @return parameter descriptor or null
     */
    @Override
    public ActionInputParameter getActionInputParameter(String name) {
        ActionInputParameter ret = requestParams.get(name);
        if (ret == null) {
            ret = pathVariables.get(name);
        }
        if (ret == null) {
            for (ActionInputParameter annotatedParameter : getInputParameters()) {
                // TODO create ActionInputParameter for bean property at property path
                // TODO field access in addition to bean?
                PropertyDescriptor pd = getPropertyDescriptorForPropertyPath(name,
                        annotatedParameter.getParameterType());
                if (pd != null) {
                    if (pd.getWriteMethod() != null) {

                        Object callValue = annotatedParameter.getValue();
                        Object propertyValue = null;
                        if (callValue != null) {
                            BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(callValue);
                            propertyValue = beanWrapper.getPropertyValue(name);
                        }
                        ret = new SpringActionInputParameter(new MethodParameter(pd.getWriteMethod(), 0),
                                propertyValue);
                    }
                    break;
                }
            }
        }
        return ret;
    }

    /**
     * Recursively navigate to return a BeanWrapper for the nested property path.
     *
     * @param propertyPath
     *         property property path, which may be nested
     * @return a BeanWrapper for the target bean
     */
    PropertyDescriptor getPropertyDescriptorForPropertyPath(String propertyPath, Class<?> propertyType) {
        int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath);
        // Handle nested properties recursively.
        if (pos > -1) {
            String nestedProperty = propertyPath.substring(0, pos);
            String nestedPath = propertyPath.substring(pos + 1);
            PropertyDescriptor propertyDescriptor = BeanUtils.getPropertyDescriptor(propertyType, nestedProperty);
            //            BeanWrapperImpl nestedBw = getNestedBeanWrapper(nestedProperty);
            return getPropertyDescriptorForPropertyPath(nestedPath, propertyDescriptor.getPropertyType());
        } else {
            return BeanUtils.getPropertyDescriptor(propertyType, propertyPath);
        }
    }

    /**
     * Parameters annotated with <code>@Input</code>.
     *
     * @return parameters or empty list
     */
    public Collection<ActionInputParameter> getInputParameters() {
        return inputParams.values();
    }

    /**
     * Gets request header info.
     *
     * @param name
     *         of the request header
     * @return request header descriptor or null
     */
    public ActionInputParameter getRequestHeader(String name) {
        return requestHeaders.get(name);
    }

    /**
     * Gets request body info.
     *
     * @return request body descriptor or null
     */
    @Override
    public ActionInputParameter getRequestBody() {
        return requestBody;
    }

    /**
     * Determines if this descriptor has a request body.
     *
     * @return true if request body is present
     */
    @Override
    public boolean hasRequestBody() {
        return requestBody != null;
    }

    /**
     * Allows to set request body descriptor.
     *
     * @param requestBody
     *         descriptor to set
     */
    public void setRequestBody(ActionInputParameter requestBody) {
        this.requestBody = requestBody;
    }

    /**
     * Gets semantic type of action, e.g. a subtype of hydra:Operation or schema:Action. Use {@link Action} on a method
     * handler to define the semantic type of an action.
     *
     * @return URL identifying the type
     */
    @Override
    public String getSemanticActionType() {
        return semanticActionType;
    }

    /**
     * Sets semantic type of action, e.g. a subtype of hydra:Operation or schema:Action.
     *
     * @param semanticActionType
     *         URL identifying the type
     */
    public void setSemanticActionType(String semanticActionType) {
        this.semanticActionType = semanticActionType;
    }

    /**
     * Determines action input parameters for required url variables.
     *
     * @return required url variables
     */
    @Override
    public Map<String, ActionInputParameter> getRequiredParameters() {
        Map<String, ActionInputParameter> ret = new HashMap<String, ActionInputParameter>();
        for (Map.Entry<String, ActionInputParameter> entry : requestParams.entrySet()) {
            ActionInputParameter annotatedParameter = entry.getValue();
            if (annotatedParameter.isRequired()) {
                ret.put(entry.getKey(), annotatedParameter);
            }
        }
        for (Map.Entry<String, ActionInputParameter> entry : pathVariables.entrySet()) {
            ActionInputParameter annotatedParameter = entry.getValue();
            ret.put(entry.getKey(), annotatedParameter);
        }
        // requestBody not supported, would have to use exploded modifier
        return ret;
    }

    /**
     * Allows to set the cardinality, i.e. specify if the action refers to a collection or a single resource. Default is
     * {@link Cardinality#SINGLE}
     *
     * @param cardinality
     *         to set
     */
    public void setCardinality(Cardinality cardinality) {
        this.cardinality = cardinality;
    }

    /**
     * Allows to decide whether or not the action refers to a collection resource.
     *
     * @return cardinality
     */
    @Override
    public Cardinality getCardinality() {
        return cardinality;
    }
}