/* * 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; } }