Skip to content

Added ControllerActionBuilder with actionFor method. #71

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>3.1.4.RELEASE</spring.version>
<spring.version>3.2.3.RELEASE</spring.version>
<logback.version>1.0.9</logback.version>
<jackson1.version>1.9.10</jackson1.version>
<jackson2.version>2.1.1</jackson2.version>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package org.springframework.hateoas;

import java.io.IOException;
import java.util.Map.Entry;

import org.springframework.hateoas.action.ActionDescriptor;
import org.springframework.hateoas.action.Input;
import org.springframework.hateoas.action.Type;
import org.springframework.hateoas.mvc.MethodParameterValue;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.FileCopyUtils;

/**
* Message converter which converts one ActionDescriptor or an array of ActionDescriptor items to an HTML page
* containing one form per ActionDescriptor.
*
* Add the following to your spring configuration to enable this converter:
*
* <pre>
* &lt;bean
* class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"&gt;
* &lt;property name="messageConverters"&gt;
* &lt;list&gt;
* &lt;ref bean="jsonConverter" /&gt;
* &lt;ref bean="htmlFormMessageConverter" /&gt;
* &lt;/list&gt;
* &lt;/property&gt;
* &lt;/bean&gt;
*
* &lt;bean id="htmlFormMessageConverter" class="org.springframework.hateoas.HtmlResourceMessageConverter"&gt;
* &lt;property name="supportedMediaTypes" value="text/html" /&gt;
* &lt;/bean&gt;
* </pre>
*
* @author Dietrich Schulten
*
*/
public class HtmlResourceMessageConverter extends AbstractHttpMessageConverter<Object> {

/** expects title */
public static final String HTML_START = "" + //
// "<?xml version='1.0' encoding='UTF-8' ?>" + // formatter
"<!DOCTYPE html>" + //
"<html xmlns='http://www.w3.org/1999/xhtml'>" + //
" <head>" + //
" <title>%s</title>" + //
" </head>" + //
" <body>";

/** expects action url, form name, form method, form h1 */
public static final String FORM_START = "" + //
" <form action='%s' name='%s' method='%s'>" + //
" <h1>%s</h1>"; //

/** expects field label, input field type and field name */
public static final String FORM_INPUT = "" + //
" <label>%s<input type='%s' name='%s' value='%s' /></label>";

/** closes the form */
public static final String FORM_END = "" + //
" <input type='submit' value='Submit' />" + //
" </form>";
public static final String HTML_END = "" + //
" </body>" + //
"</html>";

@Override
protected boolean supports(Class<?> clazz) {
final boolean ret;
if (ActionDescriptor.class == clazz || ActionDescriptor[].class == clazz) {
ret = true;
} else {
ret = false;
}
return ret;
}

@Override
protected Object readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
return new Object();
}

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

StringBuilder sb = new StringBuilder();
sb.append(String.format(HTML_START, "Input Data"));

if (t instanceof ActionDescriptor[]) {
ActionDescriptor[] descriptors = (ActionDescriptor[]) t;
for (ActionDescriptor actionDescriptor : descriptors) {
appendForm(sb, actionDescriptor);
}
} else {
ActionDescriptor actionDescriptor = (ActionDescriptor) t;
appendForm(sb, actionDescriptor);
}
sb.append(HTML_END);
FileCopyUtils.copy(sb.toString().getBytes("UTF-8"), outputMessage.getBody());

}

private void appendForm(StringBuilder sb, ActionDescriptor actionDescriptor) {
String action = actionDescriptor.getRelativeActionLink();
String formName = actionDescriptor.getResourceName();

String formH1 = "Form " + formName;
sb.append(String.format(FORM_START, action, formName, actionDescriptor.getHttpMethod().toString(), formH1));

// build the form
for (Entry<String, MethodParameterValue> requestParam : actionDescriptor.getRequestParams().entrySet()) {

String requestParamName = requestParam.getKey();
MethodParameterValue methodParameterValue = requestParam.getValue();
String inputFieldType = getInputFieldType(methodParameterValue);
String fieldLabel = requestParamName + ": ";
// TODO support list and matrix parameters?
// TODO support RequestBody mapped by object marshaler? Look at bean properties in that case instead of
// RequestParam arguments.
// TODO support valid value ranges, possible values, value constraints?
Object invocationValue = methodParameterValue.getCallValue();
sb.append(String.format(FORM_INPUT, fieldLabel, inputFieldType, requestParamName, invocationValue == null ? ""
: invocationValue.toString()));
}
sb.append(FORM_END);
}

private String getInputFieldType(MethodParameterValue methodParameterValue) {
final String ret;
Input inputAnnotation = methodParameterValue.getParameterAnnotation(Input.class);
if (inputAnnotation != null) {
ret = inputAnnotation.value().toString();
} else {
Class<?> parameterType = methodParameterValue.getParameterType();
if (Number.class.isAssignableFrom(parameterType)) {
ret = Type.NUMBER.toString();
} else {
ret = Type.TEXT.toString();
}
}
return ret;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.springframework.hateoas.action;

import java.util.HashMap;
import java.util.Map;

import org.springframework.hateoas.mvc.MethodParameterValue;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.util.UriComponents;

/**
* Describes an HTTP action, e.g. for a form that calls a Controller method which handles the request built by the form.
*
* @author Dietrich Schulten
*
*/
public class ActionDescriptor {

private UriComponents actionLink;
private Map<String, MethodParameterValue> requestParams = new HashMap<String, MethodParameterValue>();
private RequestMethod httpMethod;
private String resourceName;

/**
* Creates an action descriptor. E.g. can be used to create HTML forms.
*
* @param resourceName can be used by the action representation, e.g. to identify the action using a form name.
* @param actionLink to which the action is submitted
* @param requestMethod used during submit
*/
public ActionDescriptor(String resourceName, UriComponents actionLink, RequestMethod requestMethod) {
this.actionLink = actionLink;
this.httpMethod = requestMethod;
this.resourceName = resourceName;
}

public String getResourceName() {
return resourceName;
}

public RequestMethod getHttpMethod() {
return httpMethod;
}

public UriComponents getActionLink() {
return actionLink;
}

public String getRelativeActionLink() {
return actionLink.getPath();
}


public Map<String, MethodParameterValue> getRequestParams() {
return requestParams;
}


public void addRequestParam(String key, MethodParameterValue methodParameterValue) {
requestParams.put(key, methodParameterValue);
}

}
27 changes: 27 additions & 0 deletions src/main/java/org/springframework/hateoas/action/Input.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.springframework.hateoas.action;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Allows to mark a method parameter as Hidden, e.g. when used as a POST parameter for a form which is not supposed to
* be changed by the client.
*
* @author Dietrich Schulten
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Input {

Type value();

int max() default -1;

int min() default -1;

int step() default -1;

}
21 changes: 21 additions & 0 deletions src/main/java/org/springframework/hateoas/action/Type.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.springframework.hateoas.action;

public enum Type {
TEXT("text"), HIDDEN("hidden"), PASSWORD("password"), COLOR("color"), DATE("date"), DATETIME("datetime"), DATETIME_LOCAL(
"datetime-local"), EMAIL("email"), MONTH("month"), NUMBER("number"), RANGE("range"), SEARCH("search"), TEL("tel"), TIME(
"time"), URL("url"), WEEK("week");

private String value;

Type(String value) {
this.value = value;
}

/**
* Returns the correct html input type string value.
*/
public String toString() {
return value;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Classes supporting the creation of forms-like resources, e.g. {@link ControllerActionBuilder}
*/
package org.springframework.hateoas.action;

Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import java.util.List;
import java.util.Map;
import java.util.Arrays;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
Expand Down Expand Up @@ -201,7 +202,7 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw
}

if (bean instanceof AnnotationMethodHandlerAdapter) {
registerModule(((AnnotationMethodHandlerAdapter) bean).getMessageConverters());
registerModule(Arrays.asList(((AnnotationMethodHandlerAdapter) bean).getMessageConverters()));
}

if (bean instanceof ObjectMapper) {
Expand Down Expand Up @@ -265,14 +266,14 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) thro
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

if (bean instanceof AnnotationMethodHandlerAdapter) {
registerModule(((AnnotationMethodHandlerAdapter) bean).getMessageConverters());
}

if (bean instanceof RequestMappingHandlerAdapter) {
registerModule(((RequestMappingHandlerAdapter) bean).getMessageConverters());
}

if (bean instanceof AnnotationMethodHandlerAdapter) {
registerModule(Arrays.asList(((AnnotationMethodHandlerAdapter) bean).getMessageConverters()));
}

if (bean instanceof org.codehaus.jackson.map.ObjectMapper) {
registerModule(bean);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.aopalliance.intercept.MethodInvocation;
import org.springframework.core.MethodParameter;
Expand Down Expand Up @@ -84,7 +86,7 @@ static class BoundMethodParameter {
private final MethodParameter parameter;
private final Object value;
private final AnnotationAttribute attribute;
private final TypeDescriptor parameterTypeDecsriptor;
private final TypeDescriptor parameterTypeDescriptor;

/**
* Creates a new {@link BoundMethodParameter}
Expand All @@ -100,7 +102,7 @@ public BoundMethodParameter(MethodParameter parameter, Object value, AnnotationA
this.parameter = parameter;
this.value = value;
this.attribute = attribute;
this.parameterTypeDecsriptor = TypeDescriptor.nested(parameter, 0);
this.parameterTypeDescriptor = TypeDescriptor.nested(parameter, 0);
}

/**
Expand Down Expand Up @@ -140,7 +142,21 @@ public String asString() {
return null;
}

return (String) CONVERSION_SERVICE.convert(value, parameterTypeDecsriptor, STRING_DESCRIPTOR);
return (String) CONVERSION_SERVICE.convert(value, parameterTypeDescriptor, STRING_DESCRIPTOR);
}
}

public Map<String, MethodParameterValue> getBoundMethodParameterValues(MethodInvocation invocation) {

List<BoundMethodParameter> boundParameters = getBoundParameters(invocation);
Map<String, MethodParameterValue> result = new HashMap<String, MethodParameterValue>();
for (BoundMethodParameter boundMethodParameter : boundParameters) {
String key = boundMethodParameter.getVariableName();
MethodParameter parameter = boundMethodParameter.parameter;
Object value = boundMethodParameter.getValue();
String formatted = boundMethodParameter.asString();
result.put(key , new MethodParameterValue(parameter , value, formatted));
}
return result;
}
}
Loading