/*
 * 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.xhtml;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import de.escalon.hypermedia.PropertyUtils;
import de.escalon.hypermedia.affordance.DataType;
import de.escalon.hypermedia.spring.DefaultDocumentationProvider;
import de.escalon.hypermedia.spring.DocumentationProvider;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.*;

import javax.servlet.http.HttpServletRequest;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.*;
import java.util.Map.Entry;

/**
 * Message converter which represents a restful API as xhtml which can be used by the browser or a rest client. Converts
 * java beans and spring-hateoas Resources to xhtml and maps the body of x-www-form-urlencoded requests to RequestBody
 * method parameters. The media-type xhtml does not officially support methods other than GET or POST, therefore we must
 * "tunnel" other methods when this converter is used with the browser. Spring's {@link
 * org.springframework.web.filter.HiddenHttpMethodFilter} allows to do that with relative ease.
 *
 * @author Dietrich Schulten
 */
public class XhtmlResourceMessageConverter extends AbstractHttpMessageConverter<Object> {

    private Charset charset = Charset.forName("UTF-8");
    private String methodParam = "_method";
    private List<String> stylesheets = Collections.emptyList();

    private DocumentationProvider documentationProvider = new DefaultDocumentationProvider();

    public XhtmlResourceMessageConverter() {
        this.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_HTML, MediaType.APPLICATION_FORM_URLENCODED));
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return true;
    }

    public Object read(java.lang.reflect.Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws
            IOException, HttpMessageNotReadableException {

        final Class clazz;
        if (type instanceof Class) {
            clazz = (Class) type;
        } else if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;
            Type rawType = parameterizedType.getRawType();
            if (rawType instanceof Class) {
                clazz = (Class) rawType;
            } else {
                throw new IllegalArgumentException("unexpected raw type " + rawType);
            }
        } else {
            throw new IllegalArgumentException("unexpected type " + type);
        }
        return readInternal(clazz, inputMessage);
    }

    @Override
    protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException,
            HttpMessageNotReadableException {

        InputStream is;
        if (inputMessage instanceof ServletServerHttpRequest) {
            // this is necessary to support HiddenHttpMethodFilter
            // thanks to https://www.w3.org/html/wg/tracker/issues/195
            // but see http://dev.w3.org/html5/decision-policy/html5-2014-plan.html#issues
            // and http://cameronjones.github.io/form-http-extensions/index.html
            // and http://www.w3.org/TR/form-http-extensions/
            // TODO recognize this more safely or make the filter mandatory
            MediaType contentType = inputMessage.getHeaders()
                    .getContentType();
            Charset charset = contentType.getCharSet() != null ? contentType.getCharSet() : this.charset;
            ServletServerHttpRequest servletServerHttpRequest = (ServletServerHttpRequest) inputMessage;
            HttpServletRequest servletRequest = servletServerHttpRequest.getServletRequest();
            is = getBodyFromServletRequestParameters(servletRequest, charset.displayName(Locale.US));
        } else {
            is = inputMessage.getBody();
        }
        return readRequestBody(clazz, is, charset);
    }

    /**
     * From {@link ServletServerHttpRequest}: Use {@link javax.servlet.ServletRequest#getParameterMap()} to reconstruct
     * the body of a form 'POST' providing a predictable outcome as opposed to reading from the body, which can fail if
     * any other code has used ServletRequest to access a parameter thus causing the input stream to be "consumed".
     */
    private InputStream getBodyFromServletRequestParameters(HttpServletRequest request, String charset) throws
            IOException {

        ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
        Writer writer = new OutputStreamWriter(bos, charset);
        @SuppressWarnings("unchecked")
        Map<String, String[]> form = request.getParameterMap();
        for (Iterator<String> nameIterator = form.keySet()
                .iterator(); nameIterator.hasNext(); ) {
            String name = nameIterator.next();
            List<String> values = Arrays.asList(form.get(name));
            for (Iterator<String> valueIterator = values.iterator(); valueIterator.hasNext(); ) {
                String value = valueIterator.next();
                writer.write(URLEncoder.encode(name, charset));
                if (value != null) {
                    writer.write('=');
                    writer.write(URLEncoder.encode(value, charset));
                    if (valueIterator.hasNext()) {
                        writer.write('&');
                    }
                }
            }
            if (nameIterator.hasNext()) {
                writer.append('&');
            }
        }
        writer.flush();

        return new ByteArrayInputStream(bos.toByteArray());
    }

    private Object readRequestBody(Class<?> clazz, InputStream inputStream, Charset charset) throws
            IOException {

        String body = StreamUtils.copyToString(inputStream, charset);

        String[] pairs = StringUtils.tokenizeToStringArray(body, "&");

        MultiValueMap<String, String> formValues = new LinkedMultiValueMap<String, String>(pairs.length);

        for (String pair : pairs) {
            int idx = pair.indexOf('=');
            if (idx == -1) {
                formValues.add(URLDecoder.decode(pair, charset.name()), null);
            } else {
                String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
                String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
                formValues.add(name, value);
            }
        }

        return recursivelyCreateObject(clazz, formValues, "");
    }

    Object recursivelyCreateObject(Class<?> clazz, MultiValueMap<String, String> formValues, String parentParamName) {

        if (Map.class.isAssignableFrom(clazz)) {
            throw new IllegalArgumentException("Map not supported");
        } else if (Collection.class.isAssignableFrom(clazz)) {
            throw new IllegalArgumentException("Collection not supported");
        } else {
            try {
                Constructor[] constructors = clazz.getConstructors();
                Constructor constructor = PropertyUtils.findDefaultCtor(constructors);
                if (constructor == null) {
                    constructor = PropertyUtils.findJsonCreator(constructors, JsonCreator.class);
                }
                Assert.notNull(constructor, "no default constructor or JsonCreator found");
                int parameterCount = constructor.getParameterTypes().length;
                Object[] args = new Object[parameterCount];
                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;
                                String paramName = jsonProperty.value();
                                List<String> formValue = formValues.get(parentParamName + paramName);
                                Class<?> parameterType = parameters[paramIndex];
                                if (DataType.isSingleValueType(parameterType)) {
                                    if (formValue != null) {
                                        if (formValue.size() == 1) {
                                            args[paramIndex++] = DataType.asType(parameterType, formValue.get(0));
                                        } else {
//                                        // TODO create proper collection type
                                            throw new IllegalArgumentException("variable list not supported");
//                                        List<Object> listValue = new ArrayList<Object>();
//                                        for (String item : formValue) {
//                                            listValue.add(DataType.asType(parameterType, formValue.get(0)));
//                                        }
//                                        args[paramIndex++] = listValue;
                                        }
                                    } else {
                                        args[paramIndex++] = null;
                                    }
                                } else {
                                    args[paramIndex++] = recursivelyCreateObject(parameterType, formValues,
                                            parentParamName + paramName + ".");
                                }
                            }
                        }
                    }
                    Assert.isTrue(args.length == paramIndex, "not all constructor arguments of @JsonCreator are " +
                            "annotated with @JsonProperty");
                }
                Object ret = constructor.newInstance(args);
                BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
                PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
                for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                    Method writeMethod = propertyDescriptor.getWriteMethod();
                    String name = propertyDescriptor.getName();
                    List<String> strings = formValues.get(name);
                    if (writeMethod != null && strings != null && strings.size() == 1) {
                        writeMethod.invoke(ret, DataType.asType(propertyDescriptor.getPropertyType(), strings.get(0))
                        ); // TODO lists, consume values from ctor
                    }
                }
                return ret;
            } catch (Exception e) {
                throw new RuntimeException("Failed to instantiate bean " + clazz.getName(), e);
            }
        }
    }

    @Override
    protected void writeInternal(Object t, HttpOutputMessage outputMessage) throws IOException,
            HttpMessageNotWritableException {

        XhtmlWriter xhtmlWriter = new XhtmlWriter(new OutputStreamWriter(outputMessage.getBody(), "UTF-8"));
        xhtmlWriter.setMethodParam(methodParam);
        xhtmlWriter.setStylesheets(stylesheets);
        xhtmlWriter.setDocumentationProvider(documentationProvider);

        xhtmlWriter.beginHtml("Form");
        writeNewResource(xhtmlWriter, t);
        xhtmlWriter.endHtml();
        xhtmlWriter.flush();
    }

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

    private void writeNewResource(XhtmlWriter writer, Object object) throws IOException {
        writer.beginUnorderedList();
        writeResource(writer, object);
        writer.endUnorderedList();
    }

    /**
     * Recursively converts object to xhtml data.
     *
     * @param object
     *         to convert
     * @param writer
     *         to write to
     */
    private void writeResource(XhtmlWriter writer, Object object) {
        if (object == null) {
            return;
        }
        try {
            if (object instanceof Resource) {
                Resource<?> resource = (Resource<?>) object;
                writer.beginListItem();

                writeResource(writer, resource.getContent());
                writer.writeLinks(resource.getLinks());

                writer.endListItem();
            } else if (object instanceof Resources) {
                Resources<?> resources = (Resources<?>) object;
                // TODO set name using EVO see HypermediaSupportBeanDefinitionRegistrar

                writer.beginListItem();

                writer.beginUnorderedList();
                Collection<?> content = resources.getContent();
                writeResource(writer, content);
                writer.endUnorderedList();

                writer.writeLinks(resources.getLinks());

                writer.endListItem();
            } else if (object instanceof ResourceSupport) {
                ResourceSupport resource = (ResourceSupport) object;
                writer.beginListItem();

                writeObject(writer, resource);
                writer.writeLinks(resource.getLinks());

                writer.endListItem();
            } else if (object instanceof Collection) {
                Collection<?> collection = (Collection<?>) object;
                for (Object item : collection) {
                    writeResource(writer, item);
                }
            } else { // TODO: write li for simple objects in Resources Collection
                writeObject(writer, object);
            }
        } catch (Exception ex) {
            throw new RuntimeException("failed to transform object " + object, ex);
        }
    }

    private void beginListGroupWithItem(XhtmlWriter writer) throws IOException {
        writer.beginUnorderedList();
        writer.beginListItem();
    }

    private void endListGroupWithItem(XhtmlWriter writer) throws IOException {
        writer.endListItem();
        writer.endUnorderedList();
    }

    private void writeObject(XhtmlWriter writer, Object object) throws IOException, IllegalAccessException,
            InvocationTargetException {
        if (!DataType.isSingleValueType(object.getClass())) {
            writer.beginDl();
        }
        if (object instanceof Map) {
            Map<?, ?> map = (Map<?, ?>) object;
            for (Entry<?, ?> entry : map.entrySet()) {
                String name = entry.getKey()
                        .toString();
                Object content = entry.getValue();
                String docUrl = documentationProvider.getDocumentationUrl(name, content);
                writeObjectAttributeRecursively(writer, name, content, docUrl);
            }
        } else if (object instanceof Enum) {
            String name = ((Enum) object).name();
            String docUrl = documentationProvider.getDocumentationUrl(name, object);
            writeDdForScalarValue(writer, object);
        } else if (object instanceof Currency) {
            // TODO configurable classes which should be rendered with toString
            // or use JsonSerializer or DataType?
            String name = object.toString();
            String docUrl = documentationProvider.getDocumentationUrl(name, object);
            writeDdForScalarValue(writer, object);
        } else {
            Class<?> aClass = object.getClass();
            Map<String, PropertyDescriptor> propertyDescriptors = PropertyUtils.getPropertyDescriptors(object);
            // getFields retrieves public only
            Field[] fields = aClass.getFields();
            for (Field field : fields) {
                String name = field.getName();
                if (!propertyDescriptors.containsKey(name)) {
                    Object content = field.get(object);
                    String docUrl = documentationProvider.getDocumentationUrl(field, content);
                    //http://schema.org/performer
                    writeObjectAttributeRecursively(writer, name, content, docUrl);
                }
            }
            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);
                    writeObjectAttributeRecursively(writer, name, content, docUrl);
                }
            }
        }
        if (!DataType.isSingleValueType(object.getClass())) {
            writer.endDl();
        }
    }

    private void writeObjectAttributeRecursively(XhtmlWriter writer, String name, Object content, String documentationUrl)
            throws IOException {
        Object value = getContentAsScalarValue(content);
        if (!contentIsEmpty(content)) {
            writeDtWithDoc(writer, name, documentationUrl);
        }
        if (value != null) {
            if (value != NULL_VALUE) {
                writeDdForScalarValue(writer, value);
            }
        } else if (DataType.isSingleValueType(content.getClass())) {
            writeDdForScalarValue(writer, content.toString());
        } else {
            writer.beginDd();
            writeNewResource(writer, content);
            writer.endDd();
        }
    }

    private boolean contentIsEmpty(Object content) {
        final boolean ret;
        if (content != null) {
            if (content instanceof Collection) {
                ret = ((Collection) content).isEmpty();
            } else if (content instanceof Map) {
                ret = ((Map) content).isEmpty();
            } else if (content instanceof String) {
                ret = ((String) content).isEmpty();
            } else {
                ret = false;
            }
        } else {
            ret = true;
        }
        return ret;
    }

    private void writeDtWithDoc(XhtmlWriter writer, String name, String documentationUrl) throws IOException {
        if (documentationUrl == null) {
            writer.beginDt();
            writer.write(name);
            writer.endDt();
        } else {
            writer.beginDt();
            writer.beginAnchor(XhtmlWriter.OptionalAttributes.attr("href", documentationUrl)
                    .and("title", documentationUrl));
            writer.write(name);
            writer.endAnchor();
            writer.endDt();
        }
    }

    private void writeDdForScalarValue(XhtmlWriter writer, Object value) throws IOException {
        writer.beginDd();
        writer.write(value.toString());
        writer.endDd();
    }

    /**
     * Sets method param name for HTML PUT/DELETE/PATCH workaround.
     *
     * @param methodParam
     *         to use
     * @see org.springframework.web.filter.HiddenHttpMethodFilter
     */
    public void setMethodParam(String methodParam) {
        this.methodParam = methodParam;
    }

    /**
     * Sets css stylesheets to apply to the form.
     *
     * @param stylesheets
     *         urls of css stylesheets to include, e.g. "https://maxcdn.bootstrapcdn.com/bootstrap/3.3
     *         .4/css/bootstrap.min.css"
     */
    public void setStylesheets(List<String> stylesheets) {
        Assert.notNull(stylesheets);
        this.stylesheets = stylesheets;
    }

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

    static class NullValue {

    }

    public static final NullValue NULL_VALUE = new NullValue();

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

        if (content == null) {
            value = NULL_VALUE;
        } else if (content instanceof String || content instanceof Number || content.equals(false) || content.equals
                (true)) {
            value = content;
        }
        return value;
    }
}


/*
 * 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:

path variables

request
 * params (url query params)

request headers

request body

 *
 * @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 @Input which are not also annotated as
     * @RequestParam, @PathVariable, @RequestBody or
     * @RequestHeader.
     * 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 @Input.
     *
     * @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;
    }
}


/*
 * 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.affordance;

import javax.xml.bind.DatatypeConverter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Calendar;
import java.util.Collection;
import java.util.Currency;
import java.util.Date;

/**
 * Distinguishes and creates data types, e.g. for serialization/deserialization. Created by dschulten on 22.10.2014.
 */
public class DataType {

    /**
     * Determines if the given class holds only one data item. Can be useful to determine if a value should be rendered
     * as scalar.
     *
     * @param clazz
     *         to check
     * @return true if class is scalar
     */
    public static boolean isSingleValueType(Class<?> clazz) {
        boolean ret;
        if (isNumber(clazz)
                || isBoolean(clazz)
                || isString(clazz)
                || isEnum(clazz)
                || isDate(clazz)
                || isCalendar(clazz)
                || isCurrency(clazz)
                ) {
            ret = true;
        } else {
            ret = false;
        }
        return ret;
    }

    public static boolean isArrayOrCollection(Class<?> parameterType) {
        return (parameterType.isArray() || Collection.class.isAssignableFrom(parameterType));
    }

    public static boolean isNumber(Class<?> clazz) {
        return (
                Number.class.isAssignableFrom(clazz) ||
                        int.class == clazz ||
                        long.class == clazz ||
                        float.class == clazz ||
                        byte.class == clazz ||
                        short.class == clazz ||
                        double.class == clazz
        );
    }

    public static boolean isInteger(Class<?> clazz) {
        return (Integer.class.isAssignableFrom(clazz) || int.class == clazz);
    }

    public static boolean isLong(Class<?> clazz) {
        return (Long.class.isAssignableFrom(clazz) || long.class == clazz);
    }

    public static boolean isFloat(Class<?> clazz) {
        return (Float.class.isAssignableFrom(clazz) || float.class == clazz);
    }

    public static boolean isDouble(Class<?> clazz) {
        return (Double.class.isAssignableFrom(clazz) || double.class == clazz);
    }

    public static boolean isByte(Class<?> clazz) {
        return (Byte.class.isAssignableFrom(clazz) || byte.class == clazz);
    }

    public static boolean isShort(Class<?> clazz) {
        return (Short.class.isAssignableFrom(clazz) || short.class == clazz);
    }

    public static boolean isBoolean(Class<?> clazz) {
        return (Boolean.class.isAssignableFrom(clazz) || boolean.class == clazz);
    }

    public static boolean isEnum(Class<?> clazz) {
        return Enum.class.isAssignableFrom(clazz);
    }

    public static boolean isString(Class<?> parameterType) {
        return String.class.isAssignableFrom(parameterType);
    }

    public static boolean isBigInteger(Class<?> clazz) {
        return BigInteger.class.isAssignableFrom(clazz);
    }

    public static boolean isBigDecimal(Class<?> clazz) {
        return BigDecimal.class.isAssignableFrom(clazz);
    }

    public static boolean isDate(Class<?> clazz) {
        return Date.class.isAssignableFrom(clazz);
    }

    public static boolean isCalendar(Class<?> clazz) {
        return Calendar.class.isAssignableFrom(clazz);
    }

    public static boolean isCurrency(Class<?> clazz) {
        return Currency.class.isAssignableFrom(clazz);
    }

    public static Object asScalarValue(Object bean) {
        Class<?> type = bean.getClass();
        if (isBigInteger(type)) {
            return ((BigInteger) bean).intValue();
        } else if (isBigDecimal(type)) {
            return ((BigDecimal) bean).doubleValue();
        } else if (isCalendar(type)) {
            return ((Calendar) bean).getTime();
        } else if (isDate(type)) {
            return ((Date) bean).getTime();
        } else if (isCurrency(type)) {
            return ((Currency) bean).toString();
        } else {
            return bean;
        }
    }

    public static Object asType(Class<?> type, String string) {
        if (isBoolean(type)) {
            return Boolean.parseBoolean(string);
        } else if (isInteger(type)) {
            return Integer.parseInt(string);
        } else if (isLong(type)) {
            return Long.parseLong(string);
        } else if (isDouble(type)) {
            return Double.parseDouble(string);
        } else if (isFloat(type)) {
            return Float.parseFloat(string);
        } else if (isByte(type)) {
            return Byte.parseByte(string);
        } else if (isShort(type)) {
            return Short.parseShort(string);
        } else if (isBigInteger(type)) {
            return new BigInteger(string);
        } else if (isBigDecimal(type)) {
            return new BigDecimal(string);
        } else if (isCalendar(type)) {
            return DatatypeConverter.parseDateTime(string);
        } else if (isDate(type)) {
            if (isIsoLatin1Number(string)) {
                return new Date(Long.parseLong(string));
            } else {
                return DatatypeConverter.parseDateTime(string)
                        .getTime();
            }
        } else if (isCurrency(type)) {
            return Currency.getInstance(string);
        } else if (type.isEnum()) {
            return Enum.valueOf((Class<? extends Enum>) type, string);
        } else {
            return string;
        }
    }

    /**
     * Determines if the given string contains only 0-9 [ISO-LATIN-1] or an optional leading +/- sign.
     *
     * @param str
     *         to check
     * @return true if condition holds, false otherwise
     * @see Comparison of regex and char array performance
     * @see Character#isDigit Examples for non-ISO-Latin-1-Digits
     */
    public static boolean isIsoLatin1Number(String str) {
        if (str == null)
            return false;
        char[] data = str.toCharArray();
        if (data.length == 0)
            return false;
        int index = 0;
        if (data.length > 1 && (data[0] == '-' || data[0] == '+'))
            index = 1;
        for (; index < data.length; index++) {
            if (data[index] < '0' || data[index] > '9')
                return false;
        }
        return true;
    }
}


/*
 * 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.affordance;

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import de.escalon.hypermedia.action.Cardinality;
import org.springframework.aop.DynamicIntroductionAdvice;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.TemplateVariable;
import org.springframework.hateoas.UriTemplate;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;

import java.util.*;

import static de.escalon.hypermedia.affordance.Affordance.LinkParam.*;

/**
 * Represents an http affordance for purposes of a ReST service as described by Web Linking rfc-5988. Additionally includes {@link ActionDescriptor}s
 * for http methods and expected request bodies.
Also supports templated affordances, in which case it is represented
 * as a Link-Template Header

 *
 *
This class can be created manually or via one of the {@link de.escalon.hypermedia.spring.AffordanceBuilder#linkTo}
 * methods. In the latter case the affordance should be created with pre-expanded variables (using {@link
 * PartialUriTemplate#expand} on the given uri template). In the former case one may use {@link #expandPartially} to
 * expand the Affordance variables as far as possible, while keeping unsatisified variables.

 *
 *
Created by dschulten
 * on 07.09.2014.

 */
public class Affordance extends Link {

    enum LinkParam {
        REL("rel"), ANCHOR("anchor"), REV("rev"), HREFLANG("hreflang"), MEDIA("media"),
        TITLE("title"), TITLE_STAR("title*"), TYPE("type");

        String paramName;

        LinkParam(String paramName) {
            this.paramName = paramName;
        }

        static LinkParam valueOfParamName(String paramName) {
            LinkParam.values();
            for (LinkParam linkParam : LinkParam.values()) {
                if (linkParam.paramName.equals(paramName)) {
                    return linkParam;
                }
            }
            return null;
        }

    }

    private boolean selfRel = false;
    private List<ActionDescriptor> actionDescriptors = new ArrayList<ActionDescriptor>();
    private MultiValueMap<String, String> linkParams = new LinkedMultiValueMap<String, String>();
    private PartialUriTemplate partialUriTemplate;
    private Cardinality cardinality = Cardinality.SINGLE;
    private TypedResource collectionHolder;

    /**
     * Creates affordance. Action descriptors and link header params may be added later.
     *
     * @param uriTemplate uri or uritemplate of the affordance
     * @param rels        describing the link relation type
     */
    public Affordance(String uriTemplate, String... rels) {
        this(new PartialUriTemplate(uriTemplate), new ArrayList<ActionDescriptor>(), rels);
    }

    /**
     * Creates affordance. Rels, action descriptors and link header params may be added later.
     *
     * @param uriTemplate uri or uritemplate of the affordance
     */
    public Affordance(String uriTemplate) {
        this(uriTemplate, new String[]{});
    }

    /**
     * Creates affordance, usually for a pre-expanded uriTemplate. Link header params may be added later. Optional
     * variables will be stripped before passing it to the underlying link for compatibility with the
     * underlying {@link Link} class. Use {@link #getUriTemplateComponents()} to
     * access the base uri, query head, query tail with optional variables etc.
     *
     * @param uriTemplate       pre-expanded uri or uritemplate of the affordance
     * @param actionDescriptors describing the possible http methods on the affordance
     * @param rels              describing the link relation type
     * @see PartialUriTemplate#expand
     */
    public Affordance(PartialUriTemplate uriTemplate, List<ActionDescriptor> actionDescriptors, String... rels) {
        // Since AffordanceBuilder creates variables for undefined arguments,
        // we would get a link-template where ControllerLinkBuilder only sees a link.
        // For compatibility we strip variables deemed to be not required by the actionDescriptors before passing on
        // the template to the underlying Link. That way the href of an Affordance stays compatible with a Link that
        // has been created with ControllerLinkBuilder. Only serializers that make use of Affordance will see the
        // optional variables, too.
        // They can access the base uri, query etc. via getUriTemplateComponents.
        super(uriTemplate.stripOptionalVariables(actionDescriptors)
                .toString());
        this.partialUriTemplate = uriTemplate;

        Assert.noNullElements(rels, "null rels are not allowed");

        for (String rel : rels) {
            addRel(rel);
            if ("self".equals(rel)) {
                selfRel = true;
            }
        }
        // if any action refers to a collection resource, make the affordance a collection affordance
        for (ActionDescriptor actionDescriptor : actionDescriptors) {
            if (Cardinality.COLLECTION == actionDescriptor.getCardinality()) {
                this.cardinality = Cardinality.COLLECTION;
                break;
            }
        }
        this.actionDescriptors.addAll(actionDescriptors);
    }

    private Affordance(String uriTemplate, MultiValueMap<String, String> linkParams,
                       List<ActionDescriptor> actionDescriptors) {
        this(new PartialUriTemplate(uriTemplate), actionDescriptors); // no rels to pass
        this.linkParams = linkParams; // takes care of rels
    }

    /**
     * The relation type of the link.
     *
     * @param rel IANA-registered type or extension relation type.
     */
    public void addRel(String rel) {
        Assert.hasLength(rel);
        linkParams.add(REL.paramName, rel);
    }

    /**
     * The "type" parameter, when present, is a hint indicating what the media type of the result of dereferencing the
     * link should be.  Note that this is only a hint; for example, it does not override the Content-Type header of a
     * HTTP response obtained by actually following the link.  There MUST NOT be more than one type parameter in a link-
     * value.
     *
     * @param mediaType to set
     */
    public void setType(String mediaType) {
        if (mediaType != null)
            linkParams.set(TYPE.paramName, mediaType);
        else
            linkParams.remove(TYPE.paramName);
    }

    /**
     * The "hreflang" parameter, when present, is a hint indicating what the language of the result of dereferencing the
     * link should be.  Note that this is only a hint; for example, it does not override the Content-Language header of
     * a HTTP response obtained by actually following the link.  Multiple "hreflang" parameters on a single link- value
     * indicate that multiple languages are available from the indicated resource.
     *
     * @param hreflang to add
     */
    public void addHreflang(String hreflang) {
        Assert.hasLength(hreflang);
        linkParams.add(HREFLANG.paramName, hreflang);
    }

    /**
     * The "title" parameter, when present, is used to label the destination of a link such that it can be used as a
     * human-readable identifier (e.g., a menu entry) in the language indicated by the Content- Language header (if
     * present).  The "title" parameter MUST NOT appear more than once in a given link-value; occurrences after the
     * first MUST be ignored by parsers.
     *
     * @param title to set
     */
    public void setTitle(String title) {
        if (title != null)
            linkParams.set(TITLE.paramName, title);
        else {
            linkParams.remove(TITLE.paramName);
        }
    }

    @JsonIgnore
    public boolean isBaseUriTemplated() {
        return partialUriTemplate.asComponents()
                .isBaseUriTemplated();
    }

    /**
     * Gets the 'title' link parameter, which gives a human-readable identifier in the language indicated by
     * Content-Language header.
     *
     * @return title of link
     * @see #setTitle(String)
     */
    public String getTitle() {
        return linkParams.getFirst(TITLE.paramName);
    }

    /**
     * Bean which allows to json-unwrap map-valued properties.
     *
     * @see JsonUnwrapped not supported for Map-valued properties
     * @see Jackson tips: using @JsonAnyGetter/@JsonAnySetter to create "dyna beans"
     */
    public class DynaBean {
        protected Map<String, Object> dynaProperties = new HashMap<String, Object>();

        public Object get(String name) {
            return dynaProperties.get(name);
        }

        public void putAll(Map<String, String> map) {
            dynaProperties.putAll(map);
        }

        // "any getter" needed for serialization
        @JsonAnyGetter
        public Map<String, Object> any() {
            return dynaProperties;
        }

        @JsonAnySetter
        public void set(String name, Object value) {
            dynaProperties.put(name, value);
        }

        @Override
        public String toString() {
            return dynaProperties.toString();
        }
    }

    @JsonUnwrapped
    public DynaBean getLinkExtensions() {
        DynaBean dynaBean = new DynaBean();
        LinkedHashMap<String, String> linkExtensions = new LinkedHashMap<String, String>();
        linkExtensions.putAll(linkParams.toSingleValueMap());
        for (LinkParam linkParam : LinkParam.values()) {
            linkExtensions.remove(linkParam.paramName);
        }
        dynaBean.putAll(linkExtensions);
        return dynaBean;
    }

    /**
     * Gets the 'type' link parameter, which gives a hint which mime type can be expected when following the link.
     *
     * @return type of link
     * @see #setType(String)
     */
    public String getType() {
        return linkParams.getFirst(TYPE.paramName);
    }

    /**
     * The "title*" parameter can be used to encode the title label in a different character set, and/or contain
     * language information as per RFC-5987.  The "title*" parameter MUST NOT appear more than once in a given
     * link-value; occurrences after the first MUST be ignored by parsers.  If the parameter does not contain language
     * information, its language is indicated by the Content-Language header (when present).
     *

     * If both the "title" and "title*" parameters appear in a link-value, processors SHOULD use the "title*"
     * parameter's value.
The example below shows an instance of the Link header encoding a link title using RFC-2231
     * encoding to encode both non-ASCII characters and language information.

     *

     *     Link: </TheBook/chapter2>
     *     rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel
     *

     *
     * @param titleStar to set
     */
    public void setTitleStar(String titleStar) {
        if (titleStar != null)
            linkParams.set(TITLE_STAR.paramName, titleStar);
        else
            linkParams.remove(TITLE_STAR.paramName);
    }

    /**
     * The "media" parameter, when present, is used to indicate intended destination medium or media for style
     * information (see [W3C.REC-html401-19991224], Section 6.13).  Note that this may be updated by
     * [W3C.CR-css3-mediaqueries-20090915]).  Its value MUST be quoted if it contains a semicolon (";") or comma (","),
     * and there MUST NOT be more than one "media" parameter in a link-value.
     *
     * @param mediaDesc to set
     */
    public void setMedia(String mediaDesc) {
        if (mediaDesc != null)
            linkParams.set(MEDIA.paramName, mediaDesc);
        else
            linkParams.remove(MEDIA.paramName);
    }

    /**
     * The "rev" parameter has been used in the past to indicate that the semantics of the relationship are in the
     * reverse direction.  That is, a link from A to B with REL="X" expresses the same relationship as a link from B to
     * A with REV="X". "rev" is deprecated by this specification because it often confuses authors and readers; in most
     * cases, using a separate relation type is preferable.
     *
     * @param rev to add
     */
    public void addRev(String rev) {
        Assert.hasLength(rev);
        linkParams.add(REV.paramName, rev);
    }

    /**
     * By default, the context of a link conveyed in the Link header field is the IRI of the requested resource.
When
     * present, the anchor parameter overrides this with another URI, such as a fragment of this resource, or a third
     * resource (i.e., when the anchor value is an absolute URI).  If the anchor parameter's value is a relative URI,
     * parsers MUST resolve it as per [RFC3986], Section 5.  Note that any base URI from the body's content is not
     * applied.
     *
     * @param anchor base uri to define
     */
    public void setAnchor(String anchor) {
        if (anchor != null)
            linkParams.set(ANCHOR.paramName, anchor);
        else
            linkParams.remove(ANCHOR.paramName);
    }

    /**
     * Adds link-extension params, i.e. custom params which are not described in the web linking rfc.
     *
     * @param paramName of link-extension
     * @param values    one or more values to add
     */
    public void addLinkParam(String paramName, String... values) {
        Assert.notEmpty(values);
        for (String value : values) {
            Assert.hasLength(value);
            linkParams.add(paramName, value);
        }
    }

    /**
     * Gets header name of the affordance, either Link or Link-Template depending on the presence of template
     * variables.
     *
     * @return header name
     * @see Web Linking rfc-5988
     * @see Link-Template Header
     */
    @JsonIgnore
    public String getHeaderName() {
        String headerName;
        if (super.isTemplated()) {
            headerName = "Link-Template";
        } else {
            headerName = "Link";
        }
        return headerName;
    }

    /**
     * Affordance represented as http link header value. Note that the href may be templated, for convenience you can
     * use {@link #getHeaderName()} to ensure a Link or Link-Header is produced appropriately.
     *
     * @return Link or Link-template header value
     */
    public String asHeader() {
        StringBuilder result = new StringBuilder();
        for (Map.Entry<String, List<String>> linkParamEntry : linkParams.entrySet()) {
            if (result.length() != 0) {
                result.append("; ");
            }
            String linkParamEntryKey = linkParamEntry.getKey();
            if (REL.paramName.equals(linkParamEntryKey) || REV.paramName.equals(linkParamEntryKey)) {
                result.append(linkParamEntryKey)
                        .append("=");
                result.append("\"")
                        .append(StringUtils.collectionToDelimitedString(linkParamEntry.getValue(), " "))
                        .append("\"");
            } else {
                StringBuilder linkParams = new StringBuilder();
                for (String value : linkParamEntry.getValue()) {
                    if (linkParams.length() != 0) {
                        linkParams.append("; ");
                    }
                    linkParams.append(linkParamEntryKey)
                            .append("=");
                    linkParams.append("\"")
                            .append(value)
                            .append("\"");
                }
                result.append(linkParams);
            }
        }

        String linkHeader = "<" + partialUriTemplate.asComponents()
                .toString() + ">; ";

        return result.insert(0, linkHeader)
                .toString();
    }

    /**
     * Returns template variables contained in the underlying Link with basic distinction of required and optional
     * variables based on the variable type. If actionDescriptors are present, they should be preferred over variables
     * because they consider the handler methods to determine if a variable is required or optional.
     *
     * @return variables
     */
    @Override
    public List<TemplateVariable> getVariables() {
        return super.getVariables();
    }

    @Override
    public String toString() {
        return getHeaderName() + ": " + asHeader();
    }

    @Override
    public Affordance withRel(String rel) {
        linkParams.set(REL.paramName, rel);
        return new Affordance(this.getHref(), linkParams, actionDescriptors);
    }

    @Override
    public Affordance withSelfRel() {
        if (!linkParams.get(REL.paramName)
                .contains(Link.REL_SELF)) {
            linkParams.add(REL.paramName, Link.REL_SELF);
        }
        return new Affordance(this.getHref(), linkParams, actionDescriptors);
    }

    /**
     * Expands template variables, arguments must satisfy all required template variables, optional variables will be
     * removed.
     *
     * @param arguments to expansion in the order they appear in the template
     * @return expanded affordance
     */
    @Override
    public Affordance expand(Object... arguments) {
        UriTemplate template = new UriTemplate(partialUriTemplate.asComponents()
                .toString());
        String expanded = template.expand(arguments)
                .toASCIIString();
        return new Affordance(expanded, linkParams, actionDescriptors);
    }

    /**
     * Gets href of this link. Non-mandatory template variables will be removed for compatibility with the underlying {@link Link} class.
     * To access the full uri template including optional variables, use {@link #getUriTemplateComponents()}
     *
     * @return
     */
    @Override
    public String getHref() {
        return super.getHref();
    }

    /**
     * Gets parts of the uri template such as base uri, expanded query part, unexpanded query part etc.
     *
     * @return template component parts
     */
    @JsonIgnore
    public PartialUriTemplateComponents getUriTemplateComponents() {
        return partialUriTemplate.asComponents();
    }

    /**
     * Expands template variables, arguments must satisfy all required template variables, unsatisfied optional
     * arguments will be removed.
     *
     * @param arguments to expansion
     * @return expanded affordance
     */
    @Override
    public Affordance expand(Map<String, ? extends Object> arguments) {
        UriTemplate template = new UriTemplate(partialUriTemplate.asComponents()
                .toString());
        String expanded = template.expand(arguments)
                .toASCIIString();
        return new Affordance(expanded, linkParams, actionDescriptors);
    }

    /**
     * Expands template variables as far as possible, unsatisfied variables will remain variables. This is primarily for
     * manually created affordances. If the Affordance has been created with linkTo-methodOn, it should not be necessary
     * to expand the affordance again.
     *
     * @param arguments for expansion, in the order they appear in the template
     * @return partially expanded affordance
     */
    public Affordance expandPartially(Object... arguments) {
        return new Affordance(partialUriTemplate.expand(arguments)
                .toString(), linkParams, actionDescriptors);
    }

    /**
     * Expands template variables as far as possible, unsatisfied variables will remain variables. This is primarily for
     * manually created affordances. If the Affordance has been created with linkTo-methodOn, it should not be necessary
     * to expand the affordance again.
     *
     * @param arguments for expansion
     * @return partially expanded affordance
     */
    public Affordance expandPartially(Map<String, ? extends Object> arguments) {
        return new Affordance(partialUriTemplate.expand((Map<String, Object>) arguments)
                .toString(), linkParams, actionDescriptors);
    }

    /**
     * Allows to retrieve all rels defined for this affordance.
     *
     * @return rels
     */
    @JsonIgnore
    public List<String> getRels() {
        final List<String> rels = linkParams.get(REL.paramName);
        return rels == null ? Collections.<String>emptyList() : Collections.unmodifiableList(rels);
    }

    /**
     * Gets the rel.
     *
     * @return first defined rel or null
     */
    @Override
    public String getRel() {
        return linkParams.getFirst(REL.paramName);
    }

    /**
     * Retrieves all revs for this affordance.
     *
     * @return revs
     */
    @JsonIgnore
    public List<String> getRevs() {
        final List<String> revs = linkParams.get(REV.paramName);
        return revs == null ? Collections.<String>emptyList() : Collections.unmodifiableList(revs);
    }

    /**
     * Gets the rev.
     *
     * @return first defined rev or null
     */
    @JsonIgnore
    public String getRev() {
        return linkParams.getFirst(REV.paramName);
    }

    /**
     * Sets action descriptors.
     *
     * @param actionDescriptors to set
     */
    public void setActionDescriptors(List<ActionDescriptor> actionDescriptors) {
        if (this.actionDescriptors.isEmpty()) {
            this.actionDescriptors = actionDescriptors;
        } else {
            throw new IllegalStateException("cannot redefine existing action descriptors");
        }
    }

    /**
     * Gets action descriptors.
     *
     * @return descriptors, never null
     */
    @JsonIgnore
    public List<ActionDescriptor> getActionDescriptors() {
        return Collections.unmodifiableList(actionDescriptors);
    }

    /**
     * Determines if the affordance points to a single or a collection resource.
     *
     * @return single or collection cardinality, never null
     */
    @JsonIgnore
    public Cardinality getCardinality() {
        return cardinality;
    }

    /**
     * Determines if the affordance is a self rel.
     *
     * @return true if the affordance is a self rel
     */
    @JsonIgnore
    public boolean isSelfRel() {
        return selfRel;
    }

    /**
     * Determines if the affordance has unsatisfied required variables. This allows to decide if the affordance can also
     * be treated as a plain Link without template variables if the caller omits all optional variables. Serializers can
     * use this to render it as a resource with optional search features.
     *
     * @return true if the affordance has unsatisfied required variables
     */
    @JsonIgnore
    public boolean hasUnsatisfiedRequiredVariables() {

        for (ActionDescriptor actionDescriptor : actionDescriptors) {
            Map<String, ActionInputParameter> requiredParameters =
                    actionDescriptor.getRequiredParameters();
            for (ActionInputParameter annotatedParameter : requiredParameters.values()) {
                if (!annotatedParameter.hasValue()) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Gets collection holder. If an affordance points to a collection, there are cases where the resource that has the
     * affordance is not semantically holding the collection items, but just has a loose relationship to the
     * collection. E.g. a product "has" no orderedItems, but it may have a loose relationship to a collection of ordered
     * items where the product can be POSTed to. The thing that semantically holds ordered items is an order,
     * not a product. Hence the order would be the collection holder.
     *
     * @return collection holder
     */
    @JsonIgnore
    public TypedResource getCollectionHolder() {
        return collectionHolder;
    }

    public void setCollectionHolder(TypedResource collectionHolder) {
        this.collectionHolder = collectionHolder;
    }
}


/*
 * Copyright 2014 the original author or authors.
 *
 * 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 java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Copied over from original impl in spring-hateoas to support AffordanceBuilder.getBuilder.
 *
 * Value object to partially implement the {@literal Forwarded} header defined in RFC 7239.
 *
 * @author Oliver Gierke
 * @see http://tools.ietf.org/html/rfc7239
 */
class ForwardedHeader {

	public static String NAME = "Forwarded";
	private static final ForwardedHeader NO_HEADER = new ForwardedHeader(Collections.<String, String> emptyMap());

	private final Map<String, String> elements;

	private ForwardedHeader(Map<String, String> elements) {
		this.elements = elements;
	}

	/**
	 * Creates a new {@link ForwardedHeader} from the given source.
	 *
	 * @param source can be {@literal null}.
	 * @return
	 */
	public static ForwardedHeader of(String source) {

		if (!StringUtils.hasText(source)) {
			return NO_HEADER;
		}

		Map<String, String> elements = new HashMap<String, String>();

		for (String part : source.split(";")) {

			String[] keyValue = part.split("=");

			if (keyValue.length != 2) {
				continue;
			}

			elements.put(keyValue[0].trim(), keyValue[1].trim());
		}

		Assert.notNull(elements, "Forwarded elements must not be null!");
		Assert.isTrue(!elements.isEmpty(), "At least one forwarded element needs to be present!");

		return new ForwardedHeader(elements);
	}

	/**
	 * Returns the value defined for the {@code proto} parameter of the header.
	 *
	 * @return
	 */
	public String getProto() {
		return elements.get("proto");
	}

	/**
	 * Returns the value defined for the {@code host} parameter of the header.
	 *
	 * @return
	 */
	public String getHost() {
		return elements.get("host");
	}
}


/*
 * 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.Input;
import de.escalon.hypermedia.action.Options;
import de.escalon.hypermedia.action.Select;
import de.escalon.hypermedia.action.Type;
import de.escalon.hypermedia.affordance.ActionDescriptor;
import de.escalon.hypermedia.affordance.ActionInputParameter;
import de.escalon.hypermedia.affordance.DataType;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.web.bind.annotation.*;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.*;

/**
 * Describes a Spring MVC rest services method parameter value with recorded sample call value and input constraints.
 *
 * @author Dietrich Schulten
 */
public class SpringActionInputParameter implements ActionInputParameter {

    private final TypeDescriptor typeDescriptor;
    private final RequestBody requestBody;
    private final RequestParam requestParam;
    private final PathVariable pathVariable;
    private final RequestHeader requestHeader;
    private Input inputAnnotation;
    private MethodParameter methodParameter;
    private Object value;
    private Boolean arrayOrCollection = null;
    private Map<String, Object> inputConstraints = new HashMap<String, Object>();

    private ConversionService conversionService = new DefaultFormattingConversionService();

    /**
     * Creates action input parameter.
     *
     * @param methodParameter   to describe
     * @param value             used during sample invocation
     * @param conversionService to apply to value
     */
    public SpringActionInputParameter(MethodParameter methodParameter, Object value, ConversionService
            conversionService) {
        this.methodParameter = methodParameter;
        this.value = value;
        this.requestBody = methodParameter.getParameterAnnotation(RequestBody.class);
        this.requestParam = methodParameter.getParameterAnnotation(RequestParam.class);
        this.pathVariable = methodParameter.getParameterAnnotation(PathVariable.class);
        this.requestHeader = methodParameter.getParameterAnnotation(RequestHeader.class);
        // always determine input constraints,
        // might be a nested property which is neither requestBody, requestParam nor pathVariable
        this.inputAnnotation = methodParameter.getParameterAnnotation(Input.class);
        if (inputAnnotation != null) {
            putInputConstraint(Input.MIN, Integer.MIN_VALUE, inputAnnotation.min());
            putInputConstraint(Input.MAX, Integer.MAX_VALUE, inputAnnotation.max());
            putInputConstraint(Input.MIN_LENGTH, Integer.MIN_VALUE, inputAnnotation.minLength());
            putInputConstraint(Input.MAX_LENGTH, Integer.MAX_VALUE, inputAnnotation.maxLength());
            putInputConstraint(Input.STEP, 0, inputAnnotation.step());
            putInputConstraint(Input.PATTERN, "", inputAnnotation.pattern());
        }
        this.conversionService = conversionService;
        this.typeDescriptor = TypeDescriptor.nested(methodParameter, 0);
    }

    /**
     * Creates new ActionInputParameter with default formatting conversion service.
     *
     * @param methodParameter holding metadata about the parameter
     * @param value           during sample method invocation
     */
    public SpringActionInputParameter(MethodParameter methodParameter, Object value) {
        this(methodParameter, value, new DefaultFormattingConversionService());
    }

    private void putInputConstraint(String key, Object defaultValue, Object value) {
        if (!value.equals(defaultValue)) {
            inputConstraints.put(key, value);
        }
    }

    /**
     * The value of the parameter at sample invocation time.
     *
     * @return value, may be null
     */
    public Object getValue() {
        return value;
    }

    /**
     * The value of the parameter at sample invocation time, formatted according to conversion configuration.
     *
     * @return value, may be null
     */
    public String getValueFormatted() {
        String ret;
        if (value == null) {
            ret = null;
        } else {
            ret = (String) conversionService.convert(value, typeDescriptor, TypeDescriptor.valueOf(String.class));
        }
        return ret;
    }

    /**
     * Gets HTML5 parameter type for input field according to {@link Type} annotation.
     *
     * @return the type
     */
    @Override
    public Type getHtmlInputFieldType() {
        final Type ret;
        if (inputAnnotation == null || inputAnnotation.value() == Type.FROM_JAVA) {
            if (isArrayOrCollection() || isRequestBody()) {
                ret = null;
            } else if (DataType.isNumber(getParameterType())) {
                ret = Type.NUMBER;
            } else {
                ret = Type.TEXT;
            }
        } else {
            ret = inputAnnotation.value();
        }
        return ret;
    }

    public boolean isRequestBody() {
        return requestBody != null;
    }

    public boolean isRequestParam() {
        return requestParam != null;
    }

    public boolean isPathVariable() {
        return pathVariable != null;
    }

    public boolean isRequestHeader() {
        return requestHeader != null;
    }

    public boolean isInputParameter() {
        return inputAnnotation != null
                && requestBody == null
                && pathVariable == null
                && requestHeader == null
                && requestParam == null;
    }

    @Override
    public String getRequestHeaderName() {
        return isRequestHeader() ? requestHeader.value() : null;
    }

    /**
     * Has constraints defined via @Input annotation. Note that there might also be other kinds of
     * constraints, e.g. @Select may define values for {@link #getPossibleValues}.
     *
     * @return true if parameter is constrained
     */
    public boolean hasInputConstraints() {
        return !inputConstraints.isEmpty();
    }

    public <T extends Annotation> T getAnnotation(Class<T> annotation) {
        return methodParameter.getParameterAnnotation(annotation);
    }

    /**
     * Determines if request body input parameter has a hidden input property.
     *
     * @param property name or property path
     * @return true if hidden
     */
    @Override
    public boolean isHidden(String property) {
        Annotation[] paramAnnotations = methodParameter.getParameterAnnotations();
        Input inputAnnotation = methodParameter.getParameterAnnotation(Input.class);
        return inputAnnotation != null && arrayContains(inputAnnotation.hidden(), property);
    }

    @Override
    public boolean isReadOnly(String property) {
        return inputAnnotation != null && (!inputAnnotation.editable() || arrayContains(inputAnnotation.readOnly(),
                property));
    }

    @Override
    public boolean isIncluded(String property) {
        boolean ret;
        if (inputAnnotation == null) {
            ret = true;
        } else {
            boolean hasExplicitOrImplicitIncludes = hasExplicitOrImplicitPropertyIncludeValue();
            ret = !hasExplicitOrImplicitIncludes || containsPropertyIncludeValue(property);
        }
        return ret;
    }

    /**
     * Find out if property is included by searching through all annotations.
     *
     * @param property
     * @return
     */
    private boolean containsPropertyIncludeValue(String property) {
        return arrayContains(inputAnnotation.readOnly(), property)
                || arrayContains(inputAnnotation.hidden(), property)
                || arrayContains(inputAnnotation.include(), property);
    }

    /**
     * Has any explicit include value or might have implicit includes because there is a hidden or readOnly flag.
     *
     * @return true if explicitly or implicitly included.
     */
    private boolean hasExplicitOrImplicitPropertyIncludeValue() {
        // TODO maybe not a useful optimization
        return inputAnnotation != null && inputAnnotation.readOnly().length > 0
                || inputAnnotation.hidden().length > 0
                || inputAnnotation.include().length > 0;
    }

    /**
     * Determines if request body input parameter should be excluded, considering {@link Input#exclude}.
     *
     * @param property name or property path
     * @return true if excluded, false if no include statement found or not excluded
     */
    @Override
    public boolean isExcluded(String property) {
        return inputAnnotation != null && arrayContains(inputAnnotation.exclude(), property);
    }

    private boolean arrayContains(String[] array, String toFind) {
        if (array.length == 0) {
            return false;
        }
        for (String item : array) {
            if (toFind.equals(item)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public Object[] getPossibleValues(ActionDescriptor actionDescriptor) {
        return getPossibleValues(methodParameter, actionDescriptor);
    }

    @Override
    public Object[] getPossibleValues(Method method, int parameterIndex, ActionDescriptor actionDescriptor) {
        MethodParameter methodParameter = new MethodParameter(method, parameterIndex);
        return getPossibleValues(methodParameter, actionDescriptor);
    }

    @Override
    public Object[] getPossibleValues(Constructor constructor, int parameterIndex, ActionDescriptor
            actionDescriptor) {
        MethodParameter methodParameter = new MethodParameter(constructor, parameterIndex);
        return getPossibleValues(methodParameter, actionDescriptor);
    }

    public Object[] getPossibleValues(MethodParameter methodParameter, ActionDescriptor actionDescriptor) {
        try {
            Class<?> parameterType = methodParameter.getNestedParameterType();
            Object[] possibleValues;
            Class<?> nested;
            if (Enum[].class.isAssignableFrom(parameterType)) {
                possibleValues = parameterType.getComponentType()
                        .getEnumConstants();
            } else if (Enum.class.isAssignableFrom(parameterType)) {
                possibleValues = parameterType.getEnumConstants();
            } else if (Collection.class.isAssignableFrom(parameterType)
                    && Enum.class.isAssignableFrom(nested = TypeDescriptor.nested(methodParameter, 1)
                    .getType())) {
                possibleValues = nested.getEnumConstants();
            } else {
                Select select = methodParameter.getParameterAnnotation(Select.class);
                if (select != null) {
                    Class<? extends Options> optionsClass = select.options();
                    Options options = optionsClass.newInstance();
                    // collect call values to pass to options.get
                    List<Object> from = new ArrayList<Object>();
                    for (String paramName : select.args()) {
                        ActionInputParameter parameterValue = actionDescriptor.getActionInputParameter(paramName);
                        if (parameterValue != null) {
                            from.add(parameterValue.getValue());
                        }
                    }

                    Object[] args = from.toArray();
                    possibleValues = options.get(select.value(), args);
                } else {
                    possibleValues = new Object[0];
                }
            }
            return possibleValues;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Determines if action input parameter is an array or collection.
     *
     * @return true if array or collection
     */
    public boolean isArrayOrCollection() {
        if (arrayOrCollection == null) {
            arrayOrCollection = DataType.isArrayOrCollection(getParameterType());
        }
        return arrayOrCollection;
    }

    /**
     * Is this action input parameter required, based on the presence of a default value, the parameter annotations and
     * the kind of input parameter.
     *
     * @return true if required
     */
    public boolean isRequired() {
        boolean ret;
        if (isRequestBody()) {
            ret = requestBody.required();
        } else if (isRequestParam()) {
            ret = !(isDefined(requestParam.defaultValue()) || !requestParam.required());
        } else if (isRequestHeader()) {
            ret = !(isDefined(requestHeader.defaultValue()) || !requestHeader.required());
        } else {
            ret = true;
        }
        return ret;
    }

    private boolean isDefined(String defaultValue) {
        return !ValueConstants.DEFAULT_NONE.equals(defaultValue);
    }

    /**
     * Determines default value of request param or request header, if available.
     *
     * @return value or null
     */
    public String getDefaultValue() {
        String ret;
        if (isRequestParam()) {
            ret = isDefined(requestParam.defaultValue()) ?
                    requestParam.defaultValue() : null;
        } else if (isRequestHeader()) {
            ret = !(ValueConstants.DEFAULT_NONE.equals(requestHeader.defaultValue())) ?
                    requestHeader.defaultValue() : null;
        } else {
            ret = null;
        }
        return ret;
    }

    /**
     * Allows convenient access to multiple call values in case that this input parameter is an array or collection.
     * Make sure to check {@link #isArrayOrCollection()} before calling this method.
     *
     * @return call values or empty array
     * @throws UnsupportedOperationException if this input parameter is not an array or collection
     */
    public Object[] getValues() {
        Object[] callValues;
        if (!isArrayOrCollection()) {
            throw new UnsupportedOperationException("parameter is not an array or collection");
        }
        Object callValue = getValue();
        if (callValue == null) {
            callValues = new Object[0];
        } else {
            Class<?> parameterType = getParameterType();
            if (parameterType.isArray()) {
                callValues = (Object[]) callValue;
            } else {
                callValues = ((Collection<?>) callValue).toArray();
            }
        }
        return callValues;
    }

    /**
     * Was a sample call value recorded for this parameter?
     *
     * @return if call value is present
     */
    public boolean hasValue() {
        return value != null;
    }

    /**
     * Gets request parameter name of this action input parameter.
     *
     * @return name
     */
    @Override
    public String getParameterName() {
        String ret = null;
        if (requestParam != null) {
            String requestParamName = requestParam.value();
            if (!requestParamName.isEmpty())
                ret = requestParamName;
        }
        if (pathVariable != null) {
            String pathVariableName = pathVariable.value();
            if (!pathVariableName.isEmpty())
                ret = pathVariableName;
        }
        if (ret == null) {
            String parameterName = methodParameter.getParameterName();
            if (parameterName == null) {
                methodParameter.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer());
                ret = methodParameter.getParameterName();
            } else {
                ret = parameterName;
            }
        }
        return ret;
    }

    /**
     * Class which declares the method to which this input parameter belongs.
     *
     * @return class
     */
    public Class<?> getDeclaringClass() {
        return methodParameter.getDeclaringClass();
    }

    /**
     * Type of parameter.
     *
     * @return type
     */
    public Class<?> getParameterType() {
        return methodParameter.getParameterType();
    }

    /**
     * Generic type of parameter.
     *
     * @return generic type
     */
    public java.lang.reflect.Type getGenericParameterType() {
        return methodParameter.getGenericParameterType();
    }

    /**
     * Gets the input constraints defined for this action input parameter.
     *
     * @return constraints
     */
    public Map<String, Object> getInputConstraints() {
        return inputConstraints;
    }

    @Override
    public String toString() {
        String kind;
        if (isRequestBody()) {
            kind = "RequestBody";
        } else if (isPathVariable()) {
            kind = "PathVariable";
        } else if (isRequestParam()) {
            kind = "RequestParam";
        } else if (isRequestHeader()) {
            kind = "RequestHeader";
        } else {
            kind = "nested bean property";
        }
        return kind + (getParameterName() != null ? " " + getParameterName() : "") + ": " + (value != null ? value
                .toString() : "no value");
    }
}