From ac8c26688b648d794134c1a09e3aaa729586da82 Mon Sep 17 00:00:00 2001 From: igor Date: Thu, 14 Apr 2016 13:45:00 +0200 Subject: [PATCH 01/60] hal-forms first commit --- .../hateoas/forms/AbstractSuggest.java | 27 ++ .../hateoas/forms/AbstractSuggestBuilder.java | 25 ++ .../hateoas/forms/EmbeddedSuggestBuilder.java | 21 ++ .../hateoas/forms/FieldNotFoundException.java | 28 ++ .../hateoas/forms/FieldUtils.java | 97 ++++++ .../springframework/hateoas/forms/Form.java | 26 ++ .../hateoas/forms/FormBuilder.java | 24 ++ .../hateoas/forms/FormBuilderFactory.java | 40 +++ .../hateoas/forms/FormBuilderSupport.java | 39 +++ .../hateoas/forms/LinkSuggest.java | 21 ++ .../hateoas/forms/LinkSuggestBuilder.java | 22 ++ .../hateoas/forms/NotSupportedException.java | 11 + .../hateoas/forms/Property.java | 64 ++++ .../hateoas/forms/PropertyBuilder.java | 74 +++++ .../hateoas/forms/Suggest.java | 11 + .../hateoas/forms/SuggestBuilder.java | 14 + .../hateoas/forms/SuggestBuilderProvider.java | 54 ++++ .../hateoas/forms/Template.java | 51 +++ .../hateoas/forms/ValueSuggest.java | 53 +++ .../hateoas/forms/ValueSuggestBuilder.java | 36 +++ .../hateoas/mvc/ControllerFormBuilder.java | 215 ++++++++++++ .../mvc/ControllerFormBuilderFactory.java | 148 +++++++++ .../mvc/ControllerFormBuilderUnitTest.java | 305 ++++++++++++++++++ 23 files changed, 1406 insertions(+) create mode 100644 src/main/java/org/springframework/hateoas/forms/AbstractSuggest.java create mode 100644 src/main/java/org/springframework/hateoas/forms/AbstractSuggestBuilder.java create mode 100644 src/main/java/org/springframework/hateoas/forms/EmbeddedSuggestBuilder.java create mode 100644 src/main/java/org/springframework/hateoas/forms/FieldNotFoundException.java create mode 100644 src/main/java/org/springframework/hateoas/forms/FieldUtils.java create mode 100644 src/main/java/org/springframework/hateoas/forms/Form.java create mode 100644 src/main/java/org/springframework/hateoas/forms/FormBuilder.java create mode 100644 src/main/java/org/springframework/hateoas/forms/FormBuilderFactory.java create mode 100644 src/main/java/org/springframework/hateoas/forms/FormBuilderSupport.java create mode 100644 src/main/java/org/springframework/hateoas/forms/LinkSuggest.java create mode 100644 src/main/java/org/springframework/hateoas/forms/LinkSuggestBuilder.java create mode 100644 src/main/java/org/springframework/hateoas/forms/NotSupportedException.java create mode 100644 src/main/java/org/springframework/hateoas/forms/Property.java create mode 100644 src/main/java/org/springframework/hateoas/forms/PropertyBuilder.java create mode 100644 src/main/java/org/springframework/hateoas/forms/Suggest.java create mode 100644 src/main/java/org/springframework/hateoas/forms/SuggestBuilder.java create mode 100644 src/main/java/org/springframework/hateoas/forms/SuggestBuilderProvider.java create mode 100644 src/main/java/org/springframework/hateoas/forms/Template.java create mode 100644 src/main/java/org/springframework/hateoas/forms/ValueSuggest.java create mode 100644 src/main/java/org/springframework/hateoas/forms/ValueSuggestBuilder.java create mode 100644 src/main/java/org/springframework/hateoas/mvc/ControllerFormBuilder.java create mode 100644 src/main/java/org/springframework/hateoas/mvc/ControllerFormBuilderFactory.java create mode 100644 src/test/java/org/springframework/hateoas/mvc/ControllerFormBuilderUnitTest.java diff --git a/src/main/java/org/springframework/hateoas/forms/AbstractSuggest.java b/src/main/java/org/springframework/hateoas/forms/AbstractSuggest.java new file mode 100644 index 000000000..814483624 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/AbstractSuggest.java @@ -0,0 +1,27 @@ +package org.springframework.hateoas.forms; + +/** + * @see Suggest + */ +public class AbstractSuggest implements Suggest { + + protected final String textFieldName; + + protected final String valueFieldName; + + public AbstractSuggest(String textFieldName, String valueFieldName) { + this.textFieldName = textFieldName; + this.valueFieldName = valueFieldName; + } + + @Override + public String getValueField() { + return valueFieldName; + } + + @Override + public String getTextField() { + return textFieldName; + } + +} diff --git a/src/main/java/org/springframework/hateoas/forms/AbstractSuggestBuilder.java b/src/main/java/org/springframework/hateoas/forms/AbstractSuggestBuilder.java new file mode 100644 index 000000000..01aa624dd --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/AbstractSuggestBuilder.java @@ -0,0 +1,25 @@ +package org.springframework.hateoas.forms; + +/** + * Abstract class that helps to construct {@link Suggest} + * @see PropertyBuilder + * @see FormBuilder + */ +public abstract class AbstractSuggestBuilder implements SuggestBuilder { + + protected String textFieldName; + + protected String valueFieldName; + + public AbstractSuggestBuilder textField(String textFieldName) { + this.textFieldName = textFieldName; + return this; + } + + public AbstractSuggestBuilder valueField(String valueFieldName) { + this.valueFieldName = valueFieldName; + return this; + } + + public abstract Suggest build(); +} diff --git a/src/main/java/org/springframework/hateoas/forms/EmbeddedSuggestBuilder.java b/src/main/java/org/springframework/hateoas/forms/EmbeddedSuggestBuilder.java new file mode 100644 index 000000000..62b6a0de9 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/EmbeddedSuggestBuilder.java @@ -0,0 +1,21 @@ +package org.springframework.hateoas.forms; + +import java.util.List; + +import org.springframework.hateoas.forms.ValueSuggest.ValueSuggestType; + +/** + * Builds {@link ValueSuggest} of type {@link ValueSuggestType#EMBEDDED} + * + */ +public class EmbeddedSuggestBuilder extends ValueSuggestBuilder { + + public EmbeddedSuggestBuilder(List values) { + super(values); + } + + @Override + public Suggest build() { + return new ValueSuggest(values, textFieldName, valueFieldName, ValueSuggestType.EMBEDDED); + } +} diff --git a/src/main/java/org/springframework/hateoas/forms/FieldNotFoundException.java b/src/main/java/org/springframework/hateoas/forms/FieldNotFoundException.java new file mode 100644 index 000000000..966ef427e --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/FieldNotFoundException.java @@ -0,0 +1,28 @@ +package org.springframework.hateoas.forms; + +/** + * Exception fired by {@link FieldUtils} when a class doesn't have a field of a specified name. + * + */ +public class FieldNotFoundException extends RuntimeException { + + private static final long serialVersionUID = 2591233443652872298L; + + private Class targetClass; + + private String field; + + public FieldNotFoundException(Class targetClass, String field) { + this.targetClass = targetClass; + this.field = field; + } + + public Class getTargetClass() { + return targetClass; + } + + public String getField() { + return field; + } + +} diff --git a/src/main/java/org/springframework/hateoas/forms/FieldUtils.java b/src/main/java/org/springframework/hateoas/forms/FieldUtils.java new file mode 100644 index 000000000..49c79e6bc --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/FieldUtils.java @@ -0,0 +1,97 @@ +package org.springframework.hateoas.forms; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Map; + +import org.springframework.core.ResolvableType; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Utility class for searching fields of a class + * + */ +public class FieldUtils { + public static final String DOT = "."; + + /** + * Attempt to find the {@link Class} of the {@link Field} on the supplied class by the especified path. + * + * @param type + * @param path may be a nested path dot separated + * @return + */ + public static Class findFieldClass(Class type, String path) { + if (path.contains(DOT)) { + int firstDot = path.indexOf(DOT); + String propertyName = path.substring(0, firstDot); + Class childType = findSimpleFieldClass(type, propertyName); + if (childType == null) { + throw new FieldNotFoundException(type, propertyName); + } + return findFieldClass(childType, path.substring(firstDot + 1, path.length())); + } + else { + return findSimpleFieldClass(type, path); + } + } + + /** + * Attempt to find a {@link Field} declared in a {@link Class}. In first instance tries to find the getter + * {@link Method} of supplied fieldName. If getter is not present attempts to find for declared field. + * @param type + * @param fieldName + * @return + */ + private static Class findSimpleFieldClass(Class type, String fieldName) { + + Method method = ReflectionUtils.findMethod(type, "get" + StringUtils.capitalize(fieldName)); + ResolvableType resolvableType = null; + if (null != method) { + resolvableType = ResolvableType.forMethodReturnType(method); + } + else { + try { + resolvableType = ResolvableType.forField(type.getDeclaredField(fieldName)); + } + catch (SecurityException e) { + throw new FieldNotFoundException(type, fieldName); + } + catch (NoSuchFieldException e) { + throw new FieldNotFoundException(type, fieldName); + } + } + + return getActualType(resolvableType); + } + + /** + * Returns the {@link Class} of supplied {@link ResolvableType} considering that: + * + *
    + *
  • - if resolvableType is an array, the component type is returned
  • + *
  • - if resolvableType is a {@link Collection}, generic parameter class is returned
  • + *
  • - if resolvableType is a {@link Map}, {@link NotSupportedException} is returned
  • + *
  • - otherwise resolvableType raw class is returned
  • + *
+ * + * @param resolvableType + * @return + */ + private static Class getActualType(ResolvableType resolvableType) { + if (resolvableType.isArray()) { + return resolvableType.getComponentType().getRawClass(); + } + else if (resolvableType.asCollection() != ResolvableType.NONE) { + return resolvableType.getGeneric(0).getRawClass(); + } + else if (resolvableType.asMap() != ResolvableType.NONE) { + throw new NotSupportedException(resolvableType.getRawClass() + " is not supported"); + } + else { + return resolvableType.getRawClass(); + } + } +} diff --git a/src/main/java/org/springframework/hateoas/forms/Form.java b/src/main/java/org/springframework/hateoas/forms/Form.java new file mode 100644 index 000000000..7043377e7 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/Form.java @@ -0,0 +1,26 @@ +package org.springframework.hateoas.forms; + +import org.springframework.web.bind.annotation.RequestBody; + +/** + * HAL-FORMS {@link Template} that contains the argument marked as {@link RequestBody} + * + */ +public class Form extends Template { + + private static final long serialVersionUID = -933494757445089955L; + + private Object body; + + public Form(String href, String rel) { + super(href, rel); + } + + public void setBody(Object body) { + this.body = body; + } + + public Object getBody() { + return body; + } +} diff --git a/src/main/java/org/springframework/hateoas/forms/FormBuilder.java b/src/main/java/org/springframework/hateoas/forms/FormBuilder.java new file mode 100644 index 000000000..ae889a89f --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/FormBuilder.java @@ -0,0 +1,24 @@ +package org.springframework.hateoas.forms; + +import org.springframework.hateoas.LinkBuilder; + +/** + * Extends {@link LinkBuilder} adding de possibility of building {@link Form} instances + * + */ +public interface FormBuilder extends LinkBuilder { + + /** + * Creates the {@link Form} using the given key + * @param key + * @return + */ + Form withKey(String key); + + /** + * Creates the {@link Form} using the default key {@link Template#DEFAULT_KEY} + * @return + */ + Form withDefaultKey(); + +} diff --git a/src/main/java/org/springframework/hateoas/forms/FormBuilderFactory.java b/src/main/java/org/springframework/hateoas/forms/FormBuilderFactory.java new file mode 100644 index 000000000..db638a533 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/FormBuilderFactory.java @@ -0,0 +1,40 @@ +package org.springframework.hateoas.forms; + +import java.lang.reflect.Method; + +import org.springframework.hateoas.core.DummyInvocationUtils; +import org.springframework.hateoas.mvc.ControllerFormBuilder; + +public interface FormBuilderFactory { + /** + * Returns a {@link ControllerFormBuilder} pointing to the URI mapped to the given {@link Method} and expanding this + * mapping using the given parameters. + * + * @param method must not be {@literal null}. + * @param parameters + * @return + */ + T formTo(Method method, Object... parameters); + + /** + * Returns a {@link ControllerFormBuilder} pointing to the URI mapped to the given {@link Method} assuming it was + * invoked on an object of the given type. + * + * @param type must not be {@literal null}. + * @param method must not be {@literal null}. + * @param parameters + * @return + */ + T formTo(Class type, Method method, Object... parameters); + + /** + * Returns a {@link ControllerFormBuilder} pointing to the URI mapped to the method the result is handed into this + * method. Use {@link DummyInvocationUtils#methodOn(Class, Object...)} to obtain a dummy instance of a controller to + * record a dummy method invocation on. See {@link HalFormsLinkBuilder#linkTo(Object)} for an example. + * + * @see ControllerLinkBuilder#linkTo(Object) + * @param methodInvocationResult must not be {@literal null}. + * @return + */ + T formTo(Object methodInvocationResult); +} diff --git a/src/main/java/org/springframework/hateoas/forms/FormBuilderSupport.java b/src/main/java/org/springframework/hateoas/forms/FormBuilderSupport.java new file mode 100644 index 000000000..5a4667937 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/FormBuilderSupport.java @@ -0,0 +1,39 @@ +package org.springframework.hateoas.forms; + +import java.util.List; + +import org.springframework.hateoas.core.LinkBuilderSupport; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * Base class to implement {@link FormBuilder}s based on a Spring MVC {@link UriComponentsBuilder}. + * + */ +public abstract class FormBuilderSupport extends LinkBuilderSupport implements FormBuilder { + + public FormBuilderSupport(UriComponentsBuilder builder) { + super(builder); + } + + @Override + public Form withKey(String key) { + Form form = new Form(toUri().normalize().toASCIIString(), key); + form.setBody(getBody()); + form.setProperties(getProperties()); + form.setMethod(getMethod()); + return form; + } + + @Override + public Form withDefaultKey() { + return withKey(Template.DEFAULT_KEY); + } + + public abstract Object getBody(); + + public abstract List getProperties(); + + public abstract RequestMethod[] getMethod(); + +} diff --git a/src/main/java/org/springframework/hateoas/forms/LinkSuggest.java b/src/main/java/org/springframework/hateoas/forms/LinkSuggest.java new file mode 100644 index 000000000..2d4e03734 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/LinkSuggest.java @@ -0,0 +1,21 @@ +package org.springframework.hateoas.forms; + +import org.springframework.hateoas.Link; + +/** + * Suggested values of a {@link Property} that are loaded by a url returning a list of values. + * + */ +public class LinkSuggest extends AbstractSuggest { + + private Link link; + + public LinkSuggest(Link link, String textFieldName, String valueFieldName) { + super(textFieldName, valueFieldName); + this.link = link; + } + + public Link getLink() { + return link; + } +} diff --git a/src/main/java/org/springframework/hateoas/forms/LinkSuggestBuilder.java b/src/main/java/org/springframework/hateoas/forms/LinkSuggestBuilder.java new file mode 100644 index 000000000..c6841225f --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/LinkSuggestBuilder.java @@ -0,0 +1,22 @@ +package org.springframework.hateoas.forms; + +import org.springframework.hateoas.Link; + +/** + * Creates instances of {@link LinkSuggest} + * + */ +public class LinkSuggestBuilder extends AbstractSuggestBuilder { + + private Link link; + + public LinkSuggestBuilder(Link link) { + this.link = link; + } + + @Override + public Suggest build() { + return new LinkSuggest(link, textFieldName, valueFieldName); + } + +} diff --git a/src/main/java/org/springframework/hateoas/forms/NotSupportedException.java b/src/main/java/org/springframework/hateoas/forms/NotSupportedException.java new file mode 100644 index 000000000..c5d563d9e --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/NotSupportedException.java @@ -0,0 +1,11 @@ +package org.springframework.hateoas.forms; + +public class NotSupportedException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public NotSupportedException(String message) { + super(message); + } + +} diff --git a/src/main/java/org/springframework/hateoas/forms/Property.java b/src/main/java/org/springframework/hateoas/forms/Property.java new file mode 100644 index 000000000..e32f9dc50 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/Property.java @@ -0,0 +1,64 @@ +package org.springframework.hateoas.forms; + +/** + * Describe a parameter for the associated state transition in a HAL-FORMS document. A {@link Template} may contain a + * list of {@link Property} + * + * @see http://mamund.site44.com/misc/hal-forms/ + * + */ +public class Property { + + private String name; + + private Boolean readOnly; + + private String value; + + private String prompt; + + private String regex; + + private boolean templated; + + private Suggest suggest; + + public Property(String name, Boolean readOnly, boolean templated, String value, String prompt, String regex, + Suggest suggest) { + this.name = name; + this.readOnly = readOnly; + this.templated = templated; + this.value = value; + this.prompt = prompt; + this.regex = regex; + this.suggest = suggest; + } + + public String getName() { + return name; + } + + public Boolean isReadOnly() { + return readOnly; + } + + public String getValue() { + return value; + } + + public String getPrompt() { + return prompt; + } + + public String getRegex() { + return regex; + } + + public boolean isTemplated() { + return templated; + } + + public Suggest getSuggest() { + return suggest; + } +} diff --git a/src/main/java/org/springframework/hateoas/forms/PropertyBuilder.java b/src/main/java/org/springframework/hateoas/forms/PropertyBuilder.java new file mode 100644 index 000000000..55cb9ca0a --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/PropertyBuilder.java @@ -0,0 +1,74 @@ +package org.springframework.hateoas.forms; + +import org.springframework.hateoas.mvc.ControllerFormBuilder; + +/** + * Builder used by {@link ControllerFormBuilder} to create instances of {@link Property} + * + */ +public class PropertyBuilder { + + private String name; + + // we want to idenfify if user has setted the readonly value (default value is not false) + private Boolean readonly; + + private boolean templated; + + private String value; + + private String prompt; + + private String regex; + + private Class declaringClass; + + protected FormBuilder formBuilder; + + private SuggestBuilderProvider suggestBuilderConfigurer; + + public PropertyBuilder(String name, Class declaringClass, FormBuilder formBuilder) { + this.name = name; + this.declaringClass = declaringClass; + this.formBuilder = formBuilder; + } + + public PropertyBuilder readonly(boolean readonly) { + this.readonly = readonly; + return this; + } + + public SuggestBuilderProvider suggest() { + this.suggestBuilderConfigurer = new SuggestBuilderProvider(); + return this.suggestBuilderConfigurer; + } + + public Property build() { + return new Property(name, readonly, templated, value, prompt, regex, + suggestBuilderConfigurer != null ? suggestBuilderConfigurer.build() : null); + } + + public Class getDeclaringClass() { + return declaringClass; + } + + public PropertyBuilder regex(String regex) { + this.regex = regex; + return this; + } + + public PropertyBuilder prompt(String prompt) { + this.prompt = prompt; + return this; + } + + public PropertyBuilder value(String value) { + this.value = value; + this.templated = isTemplatedValue(value); + return this; + } + + private boolean isTemplatedValue(String value) { + return value.startsWith("{") && value.endsWith("}"); + } +} diff --git a/src/main/java/org/springframework/hateoas/forms/Suggest.java b/src/main/java/org/springframework/hateoas/forms/Suggest.java new file mode 100644 index 000000000..e64dac152 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/Suggest.java @@ -0,0 +1,11 @@ +package org.springframework.hateoas.forms; + +/** + * Define the "value" and "text" fields of an object included in a {@link Property} of a HAL-FORMS {@link Template} + * + */ +public interface Suggest { + String getValueField(); + + String getTextField(); +} diff --git a/src/main/java/org/springframework/hateoas/forms/SuggestBuilder.java b/src/main/java/org/springframework/hateoas/forms/SuggestBuilder.java new file mode 100644 index 000000000..88ba08cd5 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/SuggestBuilder.java @@ -0,0 +1,14 @@ +package org.springframework.hateoas.forms; + +/** + * Builder for creating instances of {@link Suggest} + * @see PropertyBuilder + * @see FormBuilder + */ +public interface SuggestBuilder { + public SuggestBuilder textField(String textFieldName); + + public SuggestBuilder valueField(String valueFieldName); + + public Suggest build(); +} diff --git a/src/main/java/org/springframework/hateoas/forms/SuggestBuilderProvider.java b/src/main/java/org/springframework/hateoas/forms/SuggestBuilderProvider.java new file mode 100644 index 000000000..a5cecfc84 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/SuggestBuilderProvider.java @@ -0,0 +1,54 @@ +package org.springframework.hateoas.forms; + +import java.util.Collection; +import java.util.List; + +import org.springframework.hateoas.Link; +import org.springframework.hateoas.forms.ValueSuggest.ValueSuggestType; + +/** + * Builder returned by {@link PropertyBuilder#suggest()} that provides different types of {@link SuggestBuilder} + * + */ +public class SuggestBuilderProvider { + + private SuggestBuilder suggestBuilder; + + /** + * Returns a {@link SuggestBuilder} to construct a {@link ValueSuggest} of type {@link ValueSuggestType#DIRECT} + * @param values + * @return + */ + public SuggestBuilder values(Collection values) { + this.suggestBuilder = new ValueSuggestBuilder(values); + return suggestBuilder; + } + + /** + * Returns a {@link SuggestBuilder} to construct a {@link ValueSuggest} of type {@link ValueSuggestType#EMBEDDED} + * @param values + * @return + */ + public SuggestBuilder embedded(List values) { + this.suggestBuilder = new EmbeddedSuggestBuilder(values); + return suggestBuilder; + } + + /** + * Returns a {@link SuggestBuilder} to construct a {@link LinkSuggest} + * @param link + * @return + */ + public SuggestBuilder link(Link link) { + this.suggestBuilder = new LinkSuggestBuilder(link); + return suggestBuilder; + } + + public SuggestBuilder getSuggestBuilder() { + return suggestBuilder; + } + + public Suggest build() { + return suggestBuilder != null ? suggestBuilder.build() : null; + } +} diff --git a/src/main/java/org/springframework/hateoas/forms/Template.java b/src/main/java/org/springframework/hateoas/forms/Template.java new file mode 100644 index 000000000..20d70e2c2 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/Template.java @@ -0,0 +1,51 @@ +package org.springframework.hateoas.forms; + +import java.util.List; + +import org.springframework.hateoas.Link; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * Value object for a HAL-FORMS template. Describes the available state transition details. + * @see http://mamund.site44.com/misc/hal-forms/ + */ +public class Template extends Link { + + private static final long serialVersionUID = 2593020248152501268L; + + public static final String DEFAULT_KEY = "default"; + + private List properties; + + private RequestMethod[] method; + + public Template(String href, String rel) { + super(href, rel); + } + + public void setProperties(List properties) { + this.properties = properties; + } + + public void setMethod(RequestMethod[] method) { + this.method = method; + } + + public Property getProperty(String propertyName) { + for (Property property : properties) { + if (property.getName().equals(propertyName)) { + return property; + } + } + return null; + } + + public List getProperties() { + return properties; + } + + public RequestMethod[] getMethod() { + return method; + } + +} diff --git a/src/main/java/org/springframework/hateoas/forms/ValueSuggest.java b/src/main/java/org/springframework/hateoas/forms/ValueSuggest.java new file mode 100644 index 000000000..7a5322636 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/ValueSuggest.java @@ -0,0 +1,53 @@ +package org.springframework.hateoas.forms; + +import java.util.Collection; + +import org.springframework.hateoas.Resource; + +/** + * Suggested values of a {@link Property} that are included into the response. There are two ways: include text/value + * pairs into the "suggest" attribute of {@link Property} or include a reference to a _embedded element of + * {@link Resource} + * @see ValueSuggestType + * + * @param + */ +public class ValueSuggest extends AbstractSuggest { + + private Collection values; + + private ValueSuggestType type; + + public ValueSuggest(Collection values, String textFieldName, String valueFieldName) { + this(values, textFieldName, valueFieldName, ValueSuggestType.DIRECT); + } + + public ValueSuggest(Collection values, String textFieldName, String valueFieldName, ValueSuggestType type) { + super(textFieldName, valueFieldName); + this.values = values; + this.type = type; + } + + public ValueSuggestType getType() { + return type; + } + + public Collection getValues() { + return values; + } + + /** + * Types of {@link ValueSuggest} + */ + public static enum ValueSuggestType { + /** + * Values are serialized as a list into the "suggest" property {"suggest":[{"text":"...","value":"..."},...]} + */ + DIRECT, + /** + * Values are serialized into the _embedded attribute of a {@link Resource} and "suggest.embedded" property + * references _embedded attribute {"suggest":{"embedded":"","text-field":"","value-field":""}} + */ + EMBEDDED + } +} diff --git a/src/main/java/org/springframework/hateoas/forms/ValueSuggestBuilder.java b/src/main/java/org/springframework/hateoas/forms/ValueSuggestBuilder.java new file mode 100644 index 000000000..4c226fe66 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/ValueSuggestBuilder.java @@ -0,0 +1,36 @@ +package org.springframework.hateoas.forms; + +import java.util.Collection; + +import org.springframework.util.Assert; + +public class ValueSuggestBuilder extends AbstractSuggestBuilder { + + protected Collection values; + + private Class componentType; + + public ValueSuggestBuilder(Collection values) { + this.values = values; + if (!values.isEmpty()) { + this.componentType = values.iterator().next().getClass(); + } + } + + @Override + public AbstractSuggestBuilder textField(String textFieldName) { + Assert.notNull(FieldUtils.findFieldClass(componentType, textFieldName)); + return super.textField(textFieldName); + } + + @Override + public AbstractSuggestBuilder valueField(String valueFieldName) { + Assert.notNull(FieldUtils.findFieldClass(componentType, valueFieldName)); + return super.valueField(valueFieldName); + } + + @Override + public Suggest build() { + return new ValueSuggest(values, textFieldName, valueFieldName); + } +} diff --git a/src/main/java/org/springframework/hateoas/mvc/ControllerFormBuilder.java b/src/main/java/org/springframework/hateoas/mvc/ControllerFormBuilder.java new file mode 100644 index 000000000..820cab6c0 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mvc/ControllerFormBuilder.java @@ -0,0 +1,215 @@ +package org.springframework.hateoas.mvc; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.hateoas.core.AnnotationMappingDiscoverer; +import org.springframework.hateoas.core.DummyInvocationUtils; +import org.springframework.hateoas.core.DummyInvocationUtils.LastInvocationAware; +import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation; +import org.springframework.hateoas.core.MappingDiscoverer; +import org.springframework.hateoas.forms.FieldUtils; +import org.springframework.hateoas.forms.Form; +import org.springframework.hateoas.forms.FormBuilderSupport; +import org.springframework.hateoas.forms.Property; +import org.springframework.hateoas.forms.PropertyBuilder; +import org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor.BoundMethodParameter; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriTemplate; + +/** + * Builder to ease building {@link Form} instances pointing to Spring MVC controllers. + * + */ +public class ControllerFormBuilder extends FormBuilderSupport { + + private static final MappingDiscoverer DISCOVERER = new AnnotationMappingDiscoverer(RequestMapping.class); + + private static final ControllerFormBuilderFactory FACTORY = new ControllerFormBuilderFactory(); + + private List propertyBuilders = new ArrayList(); + + private RequestMethod[] method; + + private Object requestBody; + + public ControllerFormBuilder(UriComponentsBuilder builder) { + super(builder); + } + + public ControllerFormBuilder(UriComponentsBuilder builder, RequestMethod[] method, Object requestBody, + List propertyBuilders) { + super(builder); + this.method = method; + this.requestBody = requestBody; + this.propertyBuilders = propertyBuilders; + } + + public PropertyBuilder property(String fieldName) { + Assert.notNull(fieldName, "fieldName cannot be null"); + Class fieldType = null; + if (requestBody != null) { + fieldType = FieldUtils.findFieldClass(requestBody.getClass(), fieldName); + } + + PropertyBuilder propertyBuilder = new PropertyBuilder(fieldName, fieldType, this); + propertyBuilders.add(propertyBuilder); + return propertyBuilder; + } + + /* + * @see org.springframework.hateoas.FormBuilderFactory#formTo(Method, Object...) + */ + public static ControllerFormBuilder formTo(Method method, Object... parameters) { + return formTo(method.getDeclaringClass(), method, parameters); + } + + /* + * @see org.springframework.hateoas.FormBuilderFactory#formTo(Class, Method, Object...) + */ + public static ControllerFormBuilder formTo(Class controller, Method method, Object... parameters) { + + Assert.notNull(controller, "Controller type must not be null!"); + Assert.notNull(method, "Method must not be null!"); + + UriTemplate template = new UriTemplate(DISCOVERER.getMapping(controller, method)); + URI uri = template.expand(parameters); + + // TODO: extract requestBody from parameters with info from method.getParameterAnnotations() + ControllerFormBuilder formBuilder = new ControllerFormBuilder(getBuilder()).slash(uri); + formBuilder.method(getMethod(method)); + return formBuilder; + } + + public static ControllerFormBuilder formTo(Object invocationValue) { + ControllerFormBuilder formBuilder = FACTORY.formTo(invocationValue); + + LastInvocationAware invocations = (LastInvocationAware) invocationValue; + MethodInvocation invocation = invocations.getLastInvocation(); + + Object requestBody = null; + for (BoundMethodParameter parameter : ControllerFormBuilderFactory.REQUEST_BODY_ACCESSOR + .getBoundParameters(invocation)) { + requestBody = parameter.getValue(); + } + formBuilder.body(requestBody); + formBuilder.method(getMethod(invocation.getMethod())); + + return formBuilder; + } + + public static T methodOn(Class controller, Object... parameters) { + return DummyInvocationUtils.methodOn(controller, parameters); + } + + private static RequestMethod[] getMethod(Method method) { + RequestMapping requestMapping = AnnotationUtils.findAnnotation(method, RequestMapping.class); + if (requestMapping != null && requestMapping.method().length > 0) { + return requestMapping.method(); + } + return new RequestMethod[] { RequestMethod.GET }; + } + + @Override + protected ControllerFormBuilder getThis() { + return this; + } + + @Override + protected ControllerFormBuilder createNewInstance(UriComponentsBuilder builder) { + return new ControllerFormBuilder(builder, method, requestBody, propertyBuilders); + } + + static UriComponentsBuilder getBuilder() { + + HttpServletRequest request = getCurrentRequest(); + ServletUriComponentsBuilder builder = ServletUriComponentsBuilder.fromServletMapping(request); + + String forwardedSsl = request.getHeader("X-Forwarded-Ssl"); + + if (StringUtils.hasText(forwardedSsl) && forwardedSsl.equalsIgnoreCase("on")) { + builder.scheme("https"); + } + + String host = request.getHeader("X-Forwarded-Host"); + + if (!StringUtils.hasText(host)) { + return builder; + } + + String[] hosts = StringUtils.commaDelimitedListToStringArray(host); + String hostToUse = hosts[0]; + + if (hostToUse.contains(":")) { + + String[] hostAndPort = StringUtils.split(hostToUse, ":"); + + builder.host(hostAndPort[0]); + builder.port(Integer.parseInt(hostAndPort[1])); + + } + else { + builder.host(hostToUse); + builder.port(-1); // reset port if it was forwarded from default port + } + + String port = request.getHeader("X-Forwarded-Port"); + + if (StringUtils.hasText(port)) { + builder.port(Integer.parseInt(port)); + } + + return builder; + } + + private static HttpServletRequest getCurrentRequest() { + + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + Assert.state(requestAttributes != null, "Could not find current request via RequestContextHolder"); + Assert.isInstanceOf(ServletRequestAttributes.class, requestAttributes); + HttpServletRequest servletRequest = ((ServletRequestAttributes) requestAttributes).getRequest(); + Assert.state(servletRequest != null, "Could not find current HttpServletRequest"); + return servletRequest; + } + + @Override + public List getProperties() { + List properties = new ArrayList(); + for (PropertyBuilder builder : propertyBuilders) { + properties.add(builder.build()); + } + return properties; + } + + @Override + public Object getBody() { + return requestBody; + } + + public void body(Object requestBody) { + this.requestBody = requestBody; + } + + @Override + public RequestMethod[] getMethod() { + return method; + } + + public void method(RequestMethod[] method) { + this.method = method; + } + +} diff --git a/src/main/java/org/springframework/hateoas/mvc/ControllerFormBuilderFactory.java b/src/main/java/org/springframework/hateoas/mvc/ControllerFormBuilderFactory.java new file mode 100644 index 000000000..f257433d4 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mvc/ControllerFormBuilderFactory.java @@ -0,0 +1,148 @@ +package org.springframework.hateoas.mvc; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.springframework.core.MethodParameter; +import org.springframework.hateoas.core.AnnotationAttribute; +import org.springframework.hateoas.core.AnnotationMappingDiscoverer; +import org.springframework.hateoas.core.DummyInvocationUtils.LastInvocationAware; +import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation; +import org.springframework.hateoas.core.MappingDiscoverer; +import org.springframework.hateoas.core.MethodParameters; +import org.springframework.hateoas.forms.FormBuilderFactory; +import org.springframework.hateoas.forms.FormBuilderSupport; +import org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor.BoundMethodParameter; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriTemplate; + +/** + * Factory for {@link FormBuilderSupport} instances based on the request mapping annotated on the given controller. + */ +public class ControllerFormBuilderFactory implements FormBuilderFactory { + + private static final MappingDiscoverer DISCOVERER = new AnnotationMappingDiscoverer(RequestMapping.class); + + private static final AnnotatedParametersParameterAccessor PATH_VARIABLE_ACCESSOR = new AnnotatedParametersParameterAccessor( + new AnnotationAttribute(PathVariable.class)); + + public static final AnnotatedParametersParameterAccessor REQUEST_BODY_ACCESSOR = new AnnotatedParametersParameterAccessor( + new AnnotationAttribute(RequestBody.class)); + + private static final AnnotatedParametersParameterAccessor REQUEST_PARAM_ACCESSOR = new RequestParamParameterAccessor(); + + private List uriComponentsContributors = new ArrayList(); + + @Override + public ControllerFormBuilder formTo(Method method, Object... parameters) { + return ControllerFormBuilder.formTo(method, parameters); + } + + @Override + public ControllerFormBuilder formTo(Class type, Method method, Object... parameters) { + return ControllerFormBuilder.formTo(type, method, parameters); + } + + @Override + public ControllerFormBuilder formTo(Object invocationValue) { + Assert.isInstanceOf(LastInvocationAware.class, invocationValue); + LastInvocationAware invocations = (LastInvocationAware) invocationValue; + + MethodInvocation invocation = invocations.getLastInvocation(); + Iterator classMappingParameters = invocations.getObjectParameters(); + Method method = invocation.getMethod(); + + String mapping = DISCOVERER.getMapping(invocation.getTargetType(), method); + UriComponentsBuilder builder = ControllerFormBuilder.getBuilder().path(mapping); + + UriTemplate template = new UriTemplate(mapping); + Map values = new HashMap(); + + Iterator names = template.getVariableNames().iterator(); + while (classMappingParameters.hasNext()) { + values.put(names.next(), classMappingParameters.next()); + } + + for (BoundMethodParameter parameter : PATH_VARIABLE_ACCESSOR.getBoundParameters(invocation)) { + values.put(parameter.getVariableName(), parameter.asString()); + } + + for (BoundMethodParameter parameter : REQUEST_PARAM_ACCESSOR.getBoundParameters(invocation)) { + + Object value = parameter.getValue(); + String key = parameter.getVariableName(); + + if (value instanceof Collection) { + for (Object element : (Collection) value) { + builder.queryParam(key, element); + } + } + else { + builder.queryParam(key, parameter.asString()); + } + } + + UriComponents components = applyUriComponentsContributer(builder, invocation).buildAndExpand(values); + return new ControllerFormBuilder(UriComponentsBuilder.fromUriString(components.toUriString())); + } + + protected UriComponentsBuilder applyUriComponentsContributer(UriComponentsBuilder builder, + MethodInvocation invocation) { + + MethodParameters parameters = new MethodParameters(invocation.getMethod()); + Iterator parameterValues = Arrays.asList(invocation.getArguments()).iterator(); + + for (MethodParameter parameter : parameters.getParameters()) { + Object parameterValue = parameterValues.next(); + for (UriComponentsContributor contributor : uriComponentsContributors) { + if (contributor.supportsParameter(parameter)) { + contributor.enhance(builder, parameter, parameterValue); + } + } + } + + return builder; + } + + /** + * Custom extension of {@link AnnotatedParametersParameterAccessor} for {@link RequestParam} to allow + * {@literal null} values handed in for optional request parameters. + * + * @author Oliver Gierke + */ + private static class RequestParamParameterAccessor extends AnnotatedParametersParameterAccessor { + + public RequestParamParameterAccessor() { + super(new AnnotationAttribute(RequestParam.class)); + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor#verifyParameterValue(org.springframework + * .core.MethodParameter, java.lang.Object) + */ + @Override + protected Object verifyParameterValue(MethodParameter parameter, Object value) { + + RequestParam annotation = parameter.getParameterAnnotation(RequestParam.class); + return annotation.required() && annotation.defaultValue().equals(ValueConstants.DEFAULT_NONE) + ? super.verifyParameterValue(parameter, value) : value; + } + } + +} diff --git a/src/test/java/org/springframework/hateoas/mvc/ControllerFormBuilderUnitTest.java b/src/test/java/org/springframework/hateoas/mvc/ControllerFormBuilderUnitTest.java new file mode 100644 index 000000000..de3c69359 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/mvc/ControllerFormBuilderUnitTest.java @@ -0,0 +1,305 @@ +package org.springframework.hateoas.mvc; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.hateoas.Identifiable; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; +import org.springframework.hateoas.forms.FieldNotFoundException; +import org.springframework.hateoas.forms.LinkSuggest; +import org.springframework.hateoas.forms.Property; +import org.springframework.hateoas.forms.PropertyBuilder; +import org.springframework.hateoas.forms.Template; +import org.springframework.hateoas.forms.ValueSuggest; +import org.springframework.hateoas.forms.ValueSuggest.ValueSuggestType; +import org.springframework.http.HttpEntity; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +public class ControllerFormBuilderUnitTest { + + private static final String CREATE_ITEM_FORM_KEY = "create-item"; + + protected MockHttpServletRequest request; + + protected List sizes; + + @Before + public void setUp() { + + request = new MockHttpServletRequest(); + ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(requestAttributes); + + sizes = Arrays.asList(new Size("big"), new Size("small")); + } + + @Test + public void testRequestBodyField() { + ControllerFormBuilder formBuilder = ControllerFormBuilder + .formTo(ControllerFormBuilder.methodOn(ItemController.class).create(new Item())); + PropertyBuilder propertyBuilder = formBuilder.property("size"); + + assertEquals(propertyBuilder.getDeclaringClass(), Size.class); + } + + @Test + public void testRequestBodyFieldNested() { + ControllerFormBuilder formBuilder = ControllerFormBuilder + .formTo(ControllerFormBuilder.methodOn(ItemController.class).create(new Item())); + PropertyBuilder propertyBuilder = formBuilder.property("size.id"); + + assertEquals(String.class, propertyBuilder.getDeclaringClass()); + } + + @Test + public void testRequestBodyFieldNestedList() { + ControllerFormBuilder formBuilder = ControllerFormBuilder + .formTo(ControllerFormBuilder.methodOn(OrderController.class).create(new Order())); + PropertyBuilder propertyBuilder = formBuilder.property("lineItems.milk"); + + assertEquals(Milk.class, propertyBuilder.getDeclaringClass()); + } + + @Test + public void testRequestBodyFieldNotFound() { + try { + ControllerFormBuilder formBuilder = ControllerFormBuilder + .formTo(ControllerFormBuilder.methodOn(OrderController.class).create(new Order())); + formBuilder.property("lineItems.missing"); + } + catch (FieldNotFoundException e) { + assertEquals(LineItem.class, e.getTargetClass()); + assertThat(e.getField(), is("missing")); + } + } + + @Test + public void testFormToMethod() { + ControllerFormBuilder formBuilder = ControllerFormBuilder.formTo(ItemController.class, + ItemController.class.getMethods()[1], new Item()); + + Template createItemForm = formBuilder.withKey(CREATE_ITEM_FORM_KEY); + + assertThat(createItemForm.getMethod()[0], is(RequestMethod.POST)); + + // TODO: identify @RequestBody from parameters + // assertNotNull(createItemForm.getBody()); + } + + @Test + public void testWithKey() { + + ControllerFormBuilder formBuilder = ControllerFormBuilder + .formTo(ControllerFormBuilder.methodOn(ItemController.class).create(new Item())); + + Template createItemForm = formBuilder.withKey(CREATE_ITEM_FORM_KEY); + + assertThat(createItemForm.getMethod().length, is(1)); + assertThat(createItemForm.getMethod()[0], is(RequestMethod.POST)); + assertThat(createItemForm.getRel(), is(CREATE_ITEM_FORM_KEY)); + } + + @Test + public void testPropertyAttributes() { + ControllerFormBuilder formBuilder = ControllerFormBuilder + .formTo(ControllerFormBuilder.methodOn(ItemController.class).create(new Item())); + + formBuilder.property("name").readonly(false).regex("^(true|false)$").prompt("Item name"); + Template createItemForm = formBuilder.withKey(CREATE_ITEM_FORM_KEY); + + assertThat(createItemForm.getProperties().size(), is(1)); + + Property name = createItemForm.getProperty("name"); + assertNotNull(name); + + assertFalse(name.isReadOnly()); + assertThat(name.getRegex(), is("^(true|false)$")); + assertThat(name.getPrompt(), is("Item name")); + } + + @Test + public void testPropertyTemplated() { + ControllerFormBuilder formBuilder = ControllerFormBuilder + .formTo(ControllerFormBuilder.methodOn(ItemController.class).create(new Item())); + + formBuilder.property("name").value("{propertyFromResource}"); + Template createItemForm = formBuilder.withKey(CREATE_ITEM_FORM_KEY); + + assertThat(createItemForm.getProperties().size(), is(1)); + + Property name = createItemForm.getProperty("name"); + assertNotNull(name); + + assertTrue(name.isTemplated()); + } + + @SuppressWarnings("unchecked") + @Test + public void testPropertySuggestDirectValues() { + ControllerFormBuilder formBuilder = ControllerFormBuilder + .formTo(ControllerFormBuilder.methodOn(ItemController.class).create(new Item())); + + formBuilder.property("size").suggest().values(sizes).textField("desc").valueField("name"); + + Template createItemForm = formBuilder.withDefaultKey(); + + Property size = createItemForm.getProperty("size"); + + assertNotNull(size.getSuggest()); + assertThat(size.getSuggest().getTextField(), is("desc")); + assertThat(size.getSuggest().getValueField(), is("name")); + + assertThat(size.getSuggest(), instanceOf(ValueSuggest.class)); + + ValueSuggest suggest = (ValueSuggest) size.getSuggest(); + assertThat(suggest.getType(), is(ValueSuggestType.DIRECT)); + assertThat(suggest.getValues().size(), is(2)); + assertThat(suggest.getValues().iterator().next().getId(), is("big")); + } + + @SuppressWarnings("unchecked") + @Test + public void testPropertySuggestEmbeddedValues() { + ControllerFormBuilder formBuilder = ControllerFormBuilder + .formTo(ControllerFormBuilder.methodOn(ItemController.class).create(new Item())); + + formBuilder.property("size").suggest().embedded(sizes).textField("desc").valueField("name"); + + Template createItemForm = formBuilder.withDefaultKey(); + + Property size = createItemForm.getProperty("size"); + + assertNotNull(size.getSuggest()); + assertThat(size.getSuggest().getTextField(), is("desc")); + assertThat(size.getSuggest().getValueField(), is("name")); + + assertThat(size.getSuggest(), instanceOf(ValueSuggest.class)); + + ValueSuggest suggest = (ValueSuggest) size.getSuggest(); + assertThat(suggest.getType(), is(ValueSuggestType.EMBEDDED)); + assertThat(suggest.getValues().size(), is(2)); + assertThat(suggest.getValues().iterator().next().getId(), is("big")); + } + + @Test + public void testPropertySuggestLinkValue() { + ControllerFormBuilder formBuilder = ControllerFormBuilder + .formTo(ControllerFormBuilder.methodOn(ItemController.class).create(new Item())); + + Link link = ControllerLinkBuilder.linkTo(ControllerLinkBuilder.methodOn(SizeController.class).get()) + .withRel("sizes"); + formBuilder.property("size").suggest().link(link).textField("desc").valueField("name"); + + Template createItemForm = formBuilder.withDefaultKey(); + + Property size = createItemForm.getProperty("size"); + + assertNotNull(size.getSuggest()); + assertThat(size.getSuggest().getTextField(), is("desc")); + assertThat(size.getSuggest().getValueField(), is("name")); + + assertThat(size.getSuggest(), instanceOf(LinkSuggest.class)); + + LinkSuggest suggest = (LinkSuggest) size.getSuggest(); + assertThat(suggest.getLink(), is(link)); + } + + @RequestMapping("/items") + private interface ItemController { + + @RequestMapping("") + public HttpEntity list(); + + @RequestMapping(method = RequestMethod.POST) + public HttpEntity create(@RequestBody Item item); + + @RequestMapping(value = "/{id}", method = RequestMethod.PUT) + public HttpEntity update(@PathVariable String id, @RequestBody Item item); + + } + + @RequestMapping("/sizes") + private interface SizeController { + public Resources> get(); + } + + @RequestMapping("/orders") + private interface OrderController { + @RequestMapping(method = RequestMethod.POST) + public Resource create(@RequestBody Order order); + } + + public class Size implements Identifiable { + + private String name; + + private String desc; + + public Size() { + } + + public Size(String name) { + this.name = name; + } + + public String getId() { + return name; + } + + } + + public class Item implements Identifiable { + + private Long id; + + private String name; + + private Size size; + + public Long getId() { + return id; + } + } + + public class LineItem { + + private String name; + + private int quantity; + + private Milk milk; + + private Size size; + + } + + public enum Milk { + WHOLE, SEMI; + } + + public class Order { + private final Set lineItems = new HashSet(); + + } + +} From 47d867e82e34397a27f0bd2c1ca915272a7a1325 Mon Sep 17 00:00:00 2001 From: gillarramendi Date: Fri, 15 Apr 2016 13:20:23 +0200 Subject: [PATCH 02/60] Skip warnings --- .../hateoas/mvc/ControllerFormBuilderUnitTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/java/org/springframework/hateoas/mvc/ControllerFormBuilderUnitTest.java b/src/test/java/org/springframework/hateoas/mvc/ControllerFormBuilderUnitTest.java index de3c69359..d3fe70ca5 100644 --- a/src/test/java/org/springframework/hateoas/mvc/ControllerFormBuilderUnitTest.java +++ b/src/test/java/org/springframework/hateoas/mvc/ControllerFormBuilderUnitTest.java @@ -249,6 +249,7 @@ private interface OrderController { public Resource create(@RequestBody Order order); } + @SuppressWarnings("unused") public class Size implements Identifiable { private String name; @@ -268,6 +269,7 @@ public String getId() { } + @SuppressWarnings("unused") public class Item implements Identifiable { private Long id; @@ -281,6 +283,7 @@ public Long getId() { } } + @SuppressWarnings("unused") public class LineItem { private String name; @@ -297,6 +300,7 @@ public enum Milk { WHOLE, SEMI; } + @SuppressWarnings("unused") public class Order { private final Set lineItems = new HashSet(); From 287bb8d52639c2c612436fcd17511d76b24cf7ed Mon Sep 17 00:00:00 2001 From: igor Date: Mon, 18 Apr 2016 16:51:11 +0200 Subject: [PATCH 03/60] SuggestBuilder supports value arrays --- .../hateoas/forms/SuggestBuilderProvider.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/springframework/hateoas/forms/SuggestBuilderProvider.java b/src/main/java/org/springframework/hateoas/forms/SuggestBuilderProvider.java index a5cecfc84..b0fff82e4 100644 --- a/src/main/java/org/springframework/hateoas/forms/SuggestBuilderProvider.java +++ b/src/main/java/org/springframework/hateoas/forms/SuggestBuilderProvider.java @@ -1,5 +1,6 @@ package org.springframework.hateoas.forms; +import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -24,6 +25,11 @@ public SuggestBuilder values(Collection values) { return suggestBuilder; } + public SuggestBuilder values(D[] values) { + this.suggestBuilder = new ValueSuggestBuilder(Arrays.asList(values)); + return suggestBuilder; + } + /** * Returns a {@link SuggestBuilder} to construct a {@link ValueSuggest} of type {@link ValueSuggestType#EMBEDDED} * @param values From 4d87ec41cc4c5a783f18789d934ee7b032eccdd5 Mon Sep 17 00:00:00 2001 From: igor Date: Mon, 18 Apr 2016 16:51:33 +0200 Subject: [PATCH 04/60] empty constructor Form --- src/main/java/org/springframework/hateoas/forms/Form.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/springframework/hateoas/forms/Form.java b/src/main/java/org/springframework/hateoas/forms/Form.java index 7043377e7..0c80fb909 100644 --- a/src/main/java/org/springframework/hateoas/forms/Form.java +++ b/src/main/java/org/springframework/hateoas/forms/Form.java @@ -12,6 +12,9 @@ public class Form extends Template { private Object body; + public Form() { + } + public Form(String href, String rel) { super(href, rel); } From d019fe481e0d438218a147a94a8c1e51dc897c95 Mon Sep 17 00:00:00 2001 From: igor Date: Mon, 18 Apr 2016 16:51:53 +0200 Subject: [PATCH 05/60] using resolve method --- .../org/springframework/hateoas/forms/FieldUtils.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/springframework/hateoas/forms/FieldUtils.java b/src/main/java/org/springframework/hateoas/forms/FieldUtils.java index 49c79e6bc..86022b991 100644 --- a/src/main/java/org/springframework/hateoas/forms/FieldUtils.java +++ b/src/main/java/org/springframework/hateoas/forms/FieldUtils.java @@ -82,16 +82,16 @@ private static Class findSimpleFieldClass(Class type, String fieldName) { */ private static Class getActualType(ResolvableType resolvableType) { if (resolvableType.isArray()) { - return resolvableType.getComponentType().getRawClass(); + return resolvableType.getComponentType().resolve(); } else if (resolvableType.asCollection() != ResolvableType.NONE) { - return resolvableType.getGeneric(0).getRawClass(); + return resolvableType.getGeneric(0).resolve(); } else if (resolvableType.asMap() != ResolvableType.NONE) { - throw new NotSupportedException(resolvableType.getRawClass() + " is not supported"); + throw new NotSupportedException(resolvableType.resolve() + " is not supported"); } else { - return resolvableType.getRawClass(); + return resolvableType.resolve(); } } } From ab44d6742913561020df334e0e52a2db45c7a264 Mon Sep 17 00:00:00 2001 From: gillarramendi Date: Mon, 18 Apr 2016 16:54:59 +0200 Subject: [PATCH 06/60] Template serialization --- .../hateoas/forms/AbstractSuggest.java | 9 +++ .../springframework/hateoas/forms/Form.java | 9 ++- .../hateoas/forms/LinkSuggest.java | 6 ++ .../hateoas/forms/LinkSuggestSerializer.java | 19 ++++++ .../hateoas/forms/Property.java | 4 ++ .../hateoas/forms/TemplatedResources.java | 59 +++++++++++++++++++ 6 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/springframework/hateoas/forms/LinkSuggestSerializer.java create mode 100644 src/main/java/org/springframework/hateoas/forms/TemplatedResources.java diff --git a/src/main/java/org/springframework/hateoas/forms/AbstractSuggest.java b/src/main/java/org/springframework/hateoas/forms/AbstractSuggest.java index 814483624..b216b46fc 100644 --- a/src/main/java/org/springframework/hateoas/forms/AbstractSuggest.java +++ b/src/main/java/org/springframework/hateoas/forms/AbstractSuggest.java @@ -1,12 +1,21 @@ package org.springframework.hateoas.forms; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + /** * @see Suggest */ +@JsonInclude(Include.NON_EMPTY) +@JsonIgnoreProperties({ "type" }) public class AbstractSuggest implements Suggest { + @JsonProperty("prompt-field") protected final String textFieldName; + @JsonProperty("value-field") protected final String valueFieldName; public AbstractSuggest(String textFieldName, String valueFieldName) { diff --git a/src/main/java/org/springframework/hateoas/forms/Form.java b/src/main/java/org/springframework/hateoas/forms/Form.java index 7043377e7..1beaaa235 100644 --- a/src/main/java/org/springframework/hateoas/forms/Form.java +++ b/src/main/java/org/springframework/hateoas/forms/Form.java @@ -2,10 +2,17 @@ import org.springframework.web.bind.annotation.RequestBody; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + /** * HAL-FORMS {@link Template} that contains the argument marked as {@link RequestBody} - * */ +@JsonIgnoreProperties({ "href", "body", "rel" }) +@JsonPropertyOrder({ "method", "properties" }) +@JsonInclude(Include.NON_EMPTY) public class Form extends Template { private static final long serialVersionUID = -933494757445089955L; diff --git a/src/main/java/org/springframework/hateoas/forms/LinkSuggest.java b/src/main/java/org/springframework/hateoas/forms/LinkSuggest.java index 2d4e03734..989aee586 100644 --- a/src/main/java/org/springframework/hateoas/forms/LinkSuggest.java +++ b/src/main/java/org/springframework/hateoas/forms/LinkSuggest.java @@ -2,12 +2,17 @@ import org.springframework.hateoas.Link; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + /** * Suggested values of a {@link Property} that are loaded by a url returning a list of values. * */ public class LinkSuggest extends AbstractSuggest { + @JsonSerialize(using = LinkSuggestSerializer.class) + @JsonProperty("href") private Link link; public LinkSuggest(Link link, String textFieldName, String valueFieldName) { @@ -18,4 +23,5 @@ public LinkSuggest(Link link, String textFieldName, String valueFieldName) { public Link getLink() { return link; } + } diff --git a/src/main/java/org/springframework/hateoas/forms/LinkSuggestSerializer.java b/src/main/java/org/springframework/hateoas/forms/LinkSuggestSerializer.java new file mode 100644 index 000000000..719b6f9f5 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/LinkSuggestSerializer.java @@ -0,0 +1,19 @@ +package org.springframework.hateoas.forms; + +import java.io.IOException; + +import org.springframework.hateoas.Link; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +public class LinkSuggestSerializer extends JsonSerializer { + @Override + public void serialize(Link value, JsonGenerator jgen, SerializerProvider provider) throws IOException, + JsonProcessingException { + jgen.writeString(value.getHref()); + } + +} diff --git a/src/main/java/org/springframework/hateoas/forms/Property.java b/src/main/java/org/springframework/hateoas/forms/Property.java index e32f9dc50..de23b80bb 100644 --- a/src/main/java/org/springframework/hateoas/forms/Property.java +++ b/src/main/java/org/springframework/hateoas/forms/Property.java @@ -1,5 +1,8 @@ package org.springframework.hateoas.forms; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + /** * Describe a parameter for the associated state transition in a HAL-FORMS document. A {@link Template} may contain a * list of {@link Property} @@ -7,6 +10,7 @@ * @see http://mamund.site44.com/misc/hal-forms/ * */ +@JsonInclude(Include.NON_EMPTY) public class Property { private String name; diff --git a/src/main/java/org/springframework/hateoas/forms/TemplatedResources.java b/src/main/java/org/springframework/hateoas/forms/TemplatedResources.java new file mode 100644 index 000000000..862c1bf13 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/forms/TemplatedResources.java @@ -0,0 +1,59 @@ +package org.springframework.hateoas.forms; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.hateoas.Link; +import org.springframework.hateoas.Resources; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TemplatedResources extends Resources { + + @JsonProperty("_templates") + @JsonInclude(Include.NON_EMPTY) + private List