### Eclipse Workspace Patch 1.0 #P resteasy Index: resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/ListFormInjector.java =================================================================== --- resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/ListFormInjector.java (revision 0) +++ resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/ListFormInjector.java (revision 0) @@ -0,0 +1,34 @@ +package org.jboss.resteasy.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import org.jboss.resteasy.spi.ResteasyProviderFactory; + +/** + * Can inject lists. + */ +public class ListFormInjector extends AbstractCollectionFormInjector { + + /** + * Constructor. + */ + public ListFormInjector(Class collectionType, Class genericType, String prefix, ResteasyProviderFactory factory) { + super(collectionType, genericType, prefix, Pattern.compile("^" + prefix + "\\[(\\d+)\\]"), factory); + } + + /** + * {@inheritDoc} + * @return ArrayList + */ + @Override + protected List createInstance(Class collectionType) { + return new ArrayList(); + } + + /** {@inheritDoc} */ + @Override + protected void addTo(List collection, String key, Object value) { + collection.add(Integer.parseInt(key), value); + } +} Index: resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/InjectorFactoryImpl.java =================================================================== --- resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/InjectorFactoryImpl.java (revision 1326) +++ resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/InjectorFactoryImpl.java (working copy) @@ -21,7 +21,10 @@ import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; import static org.jboss.resteasy.util.FindAnnotation.*; @@ -72,12 +75,13 @@ HeaderParam header; MatrixParam matrix; PathParam uriParam; + Form form; CookieParam cookie; FormParam formParam; Suspend suspend; - if ((query = findAnnotation(annotations, QueryParam.class)) != null) + if ((query = findAnnotation(annotations, QueryParam.class)) != null) { return new QueryParamInjector(type, genericType, injectTarget, query.value(), defaultVal, encode, annotations, providerFactory); } @@ -97,9 +101,22 @@ { return new PathParamInjector(type, genericType, injectTarget, uriParam.value(), defaultVal, encode, annotations, providerFactory); } - else if (findAnnotation(annotations, Form.class) != null) + else if ((form = findAnnotation(annotations, Form.class)) != null) { - return new FormInjector(type, providerFactory); + String prefix = form.prefix(); + if (prefix.length()> 0) { + if (genericType instanceof ParameterizedType) { + ParameterizedType pType = (ParameterizedType) genericType; + if (isA(List.class, pType)) { + return new ListFormInjector(type, getArgumentType(pType, 0), prefix, providerFactory); + } + if (isA(Map.class, pType)) { + return new MapFormInjector(type, getArgumentType(pType, 0), getArgumentType(pType, 1), prefix, providerFactory); + } + } + return new PrefixedFormInjector(type, prefix, providerFactory); + } + return new FormInjector(type, providerFactory); } else if ((matrix = findAnnotation(annotations, MatrixParam.class)) != null) { @@ -123,4 +140,18 @@ } } + /** + * Is the genericType of a certain class? + */ + private boolean isA(Class clazz, ParameterizedType pType) { + return clazz.isAssignableFrom((Class) pType.getRawType()); + } + + /** + * Gets the index-th type argument. + */ + private Class getArgumentType(ParameterizedType pType, int index) { + return (Class) pType.getActualTypeArguments()[index]; + } + } Index: resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/form/ComplexFormTest.java =================================================================== --- resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/form/ComplexFormTest.java (revision 0) +++ resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/form/ComplexFormTest.java (revision 0) @@ -0,0 +1,71 @@ +package org.jboss.resteasy.test.form; + +import static junit.framework.Assert.*; + +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.annotations.Form; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.jboss.resteasy.test.BaseResourceTest; +import org.junit.Before; +import org.junit.Test; + +public class ComplexFormTest extends BaseResourceTest { + + public static class Person { + + @FormParam("name") + private String name; + + @Form(prefix="invoice") + private Address invoice; + + @Form(prefix="shipping") + private Address shipping; + + @Override + public String toString() { + return new StringBuilder("name:'").append(name).append("', invoice:'").append(invoice.street).append("', shipping:'").append(shipping.street).append("'").toString(); + } + } + + public static class Address { + + @FormParam("street") + private String street; + } + + @Path("person") + public static class MyResource { + + @POST + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public String post (@Form Person p) { + return p.toString(); + } + } + + @Before + public void register () { + addPerRequestResource(MyResource.class); + } + + @Test + public void shouldSupportNestedForm() throws Exception { + MockHttpResponse response = new MockHttpResponse(); + MockHttpRequest request = MockHttpRequest.post("person").accept(MediaType.TEXT_PLAIN).contentType(MediaType.APPLICATION_FORM_URLENCODED); + request.addFormHeader("name", "John Doe"); + request.addFormHeader("invoice.street", "Main Street"); + request.addFormHeader("shipping.street", "Station Street"); + dispatcher.invoke(request, response); + + assertEquals("name:'John Doe', invoice:'Main Street', shipping:'Station Street'", response.getContentAsString()); + } +} Index: resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/DelegatingMultivaludMap.java =================================================================== --- resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/DelegatingMultivaludMap.java (revision 0) +++ resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/DelegatingMultivaludMap.java (revision 0) @@ -0,0 +1,161 @@ +package org.jboss.resteasy.util; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.ws.rs.core.MultivaluedMap; + +/** + * {@link MultivaluedMap} implementation that delegates to another instance. + * Convenience class for {@link MultivaluedMap} enhancements that don't want to implement all methods. + * + * @param The type of keys in the map. + * @param The type of values in the lists in the map. + */ +public class DelegatingMultivaludMap implements MultivaluedMap { + + private final MultivaluedMap delegate; + + public DelegatingMultivaludMap(MultivaluedMap delegate) { + this.delegate = delegate; + } + + /** + * @see javax.ws.rs.core.MultivaluedMap#putSingle(java.lang.Object, java.lang.Object) + */ + @Override + public void putSingle(K key, V value) { + delegate.putSingle(key, value); + } + + /** + * @see javax.ws.rs.core.MultivaluedMap#add(java.lang.Object, java.lang.Object) + */ + @Override + public void add(K key, V value) { + delegate.add(key, value); + } + + /** + * @see javax.ws.rs.core.MultivaluedMap#getFirst(java.lang.Object) + */ + @Override + public V getFirst(K key) { + return delegate.getFirst(key); + } + + /** + * @see java.util.Map#size() + */ + @Override + public int size() { + return delegate.size(); + } + + /** + * @see java.util.Map#isEmpty() + */ + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + /** + * @see java.util.Map#containsKey(java.lang.Object) + */ + @Override + public boolean containsKey(Object key) { + return delegate.containsKey(key); + } + + /** + * @see java.util.Map#containsValue(java.lang.Object) + */ + @Override + public boolean containsValue(Object value) { + return delegate.containsValue(value); + } + + /** + * @see java.util.Map#get(java.lang.Object) + */ + @Override + public List get(Object key) { + return delegate.get(key); + } + + /** + * @see java.util.Map#put(java.lang.Object, java.lang.Object) + */ + @Override + public List put(K key, List value) { + return delegate.put(key, value); + } + + /** + * @see java.util.Map#remove(java.lang.Object) + */ + @Override + public List remove(Object key) { + return delegate.remove(key); + } + + /** + * @see java.util.Map#putAll(java.util.Map) + */ + @Override + public void putAll(Map> m) { + delegate.putAll(m); + } + + /** + * @see java.util.Map#clear() + */ + @Override + public void clear() { + delegate.clear(); + } + + /** + * @see java.util.Map#keySet() + */ + @Override + public Set keySet() { + return delegate.keySet(); + } + + /** + * @see java.util.Map#values() + */ + @Override + public Collection> values() { + return delegate.values(); + } + + /** + * @see java.util.Map#entrySet() + */ + @Override + public Set>> entrySet() { + return delegate.entrySet(); + } + + /** + * @see java.util.Map#equals(java.lang.Object) + */ + @Override + public boolean equals(Object o) { + return delegate.equals(o); + } + + /** + * @see java.util.Map#hashCode() + */ + @Override + public int hashCode() { + return delegate.hashCode(); + } + +} Index: resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/AbstractCollectionFormInjector.java =================================================================== --- resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/AbstractCollectionFormInjector.java (revision 0) +++ resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/AbstractCollectionFormInjector.java (revision 0) @@ -0,0 +1,76 @@ +package org.jboss.resteasy.core; + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.ws.rs.core.MultivaluedMap; + +import org.jboss.resteasy.spi.HttpRequest; +import org.jboss.resteasy.spi.HttpResponse; +import org.jboss.resteasy.spi.ResteasyProviderFactory; + +/** + * Abstract implementation of {@link NestedFormInjector} that can inject collections. + * + * @param The type of collection that will be created. + */ +public abstract class AbstractCollectionFormInjector extends PrefixedFormInjector { + + private final Class collectionType; + + private final Pattern pattern; + + /** + * Creates an injector to inject a collection. + * + * @param collectionType The type of collection to return. + * @param genericType The type of elements in the collection. + * @param pattern The pattern that a field name should follow to be a part of this collection. The first group in the pattern must be the index. + */ + protected AbstractCollectionFormInjector(Class collectionType, Class genericType, String prefix, Pattern pattern, ResteasyProviderFactory factory) { + super(genericType, prefix, factory); + this.collectionType = collectionType; + this.pattern = pattern; + } + + /** + * {@inheritDoc} Creates a collection instance and fills it with content by using the super implementation. + */ + @Override + public Object inject(HttpRequest request, HttpResponse response) { + T result = createInstance(collectionType); + for (String collectionPrefix : findMatchingPrefixesWithNoneEmptyValues(request.getDecodedFormParameters())) { + Matcher matcher = pattern.matcher(collectionPrefix); + matcher.matches(); + String key = matcher.group(1); + addTo(result, key, super.doInject(collectionPrefix, request, response)); + } + return result; + } + + /** + * Finds all field names that follow the pattern. + */ + private Set findMatchingPrefixesWithNoneEmptyValues(MultivaluedMap parameters) { + final HashSet result = new HashSet(); + for (String parameterName : parameters.keySet()) { + final Matcher matcher = pattern.matcher(parameterName); + if (matcher.lookingAt() && hasValue(parameters.get(parameterName))) { + result.add(matcher.group(0)); + } + } + return result; + } + + /** + * Creates an instance of the collection type. + */ + protected abstract T createInstance(Class collectionType); + + /** + * Adds the item to the collection. + */ + protected abstract void addTo(T collection, String key, Object value); +} Index: resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/DelegatingHttpRequest.java =================================================================== --- resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/DelegatingHttpRequest.java (revision 0) +++ resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/DelegatingHttpRequest.java (revision 0) @@ -0,0 +1,163 @@ +package org.jboss.resteasy.util; + +import java.io.InputStream; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.UriInfo; + +import org.jboss.resteasy.spi.AsynchronousResponse; +import org.jboss.resteasy.spi.HttpRequest; + +/** + * {@link HttpRequest} implementation that wraps another {@link HttpRequest} and delegates all calls to the wrapped instance. + * Convenience class for other implementations that need to extend the functionality of {@link HttpRequest} without having to implement all methods. + */ +public class DelegatingHttpRequest implements HttpRequest { + + private final HttpRequest delegate; + + /** + * Constructor setting the delegate. + */ + public DelegatingHttpRequest(HttpRequest delegate) { + this.delegate = delegate; + } + + /** + * @see org.jboss.resteasy.spi.HttpRequest#getHttpHeaders() + */ + @Override + public HttpHeaders getHttpHeaders() { + return delegate.getHttpHeaders(); + } + + /** + * @see org.jboss.resteasy.spi.HttpRequest#getInputStream() + */ + @Override + public InputStream getInputStream() { + return delegate.getInputStream(); + } + + /** + * @see org.jboss.resteasy.spi.HttpRequest#setInputStream(java.io.InputStream) + */ + @Override + public void setInputStream(InputStream stream) { + delegate.setInputStream(stream); + } + + /** + * @see org.jboss.resteasy.spi.HttpRequest#getUri() + */ + @Override + public UriInfo getUri() { + return delegate.getUri(); + } + + /** + * @see org.jboss.resteasy.spi.HttpRequest#getHttpMethod() + */ + @Override + public String getHttpMethod() { + return delegate.getHttpMethod(); + } + + /** + * @see org.jboss.resteasy.spi.HttpRequest#getPreprocessedPath() + */ + @Override + public String getPreprocessedPath() { + return delegate.getPreprocessedPath(); + } + + /** + * @see org.jboss.resteasy.spi.HttpRequest#setPreprocessedPath(java.lang.String) + */ + @Override + public void setPreprocessedPath(String path) { + delegate.setPreprocessedPath(path); + } + + /** + * @see org.jboss.resteasy.spi.HttpRequest#getFormParameters() + */ + @Override + public MultivaluedMap getFormParameters() { + return delegate.getFormParameters(); + } + + /** + * @see org.jboss.resteasy.spi.HttpRequest#getDecodedFormParameters() + */ + @Override + public MultivaluedMap getDecodedFormParameters() { + return delegate.getDecodedFormParameters(); + } + + /** + * @see org.jboss.resteasy.spi.HttpRequest#getAttribute(java.lang.String) + */ + @Override + public Object getAttribute(String attribute) { + return delegate.getAttribute(attribute); + } + + /** + * @see org.jboss.resteasy.spi.HttpRequest#setAttribute(java.lang.String, java.lang.Object) + */ + @Override + public void setAttribute(String name, Object value) { + delegate.setAttribute(name, value); + } + + /** + * @see org.jboss.resteasy.spi.HttpRequest#removeAttribute(java.lang.String) + */ + @Override + public void removeAttribute(String name) { + delegate.removeAttribute(name); + } + + /** + * @see org.jboss.resteasy.spi.HttpRequest#isInitial() + */ + @Override + public boolean isInitial() { + return delegate.isInitial(); + } + + /** + * @see org.jboss.resteasy.spi.HttpRequest#isSuspended() + */ + @Override + public boolean isSuspended() { + return delegate.isSuspended(); + } + + /** + * @see org.jboss.resteasy.spi.HttpRequest#createAsynchronousResponse(long) + */ + @Override + public AsynchronousResponse createAsynchronousResponse(long suspendTimeout) { + return delegate.createAsynchronousResponse(suspendTimeout); + } + + /** + * @see org.jboss.resteasy.spi.HttpRequest#getAsynchronousResponse() + */ + @Override + public AsynchronousResponse getAsynchronousResponse() { + return delegate.getAsynchronousResponse(); + } + + /** + * @see org.jboss.resteasy.spi.HttpRequest#initialRequestThreadFinished() + */ + @Override + public void initialRequestThreadFinished() { + delegate.initialRequestThreadFinished(); + } + +} Index: resteasy-jaxrs/src/main/java/org/jboss/resteasy/annotations/Form.java =================================================================== --- resteasy-jaxrs/src/main/java/org/jboss/resteasy/annotations/Form.java (revision 1326) +++ resteasy-jaxrs/src/main/java/org/jboss/resteasy/annotations/Form.java (working copy) @@ -33,4 +33,6 @@ public @interface Form { + String prefix() default ""; + } Index: resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/MapFormInjector.java =================================================================== --- resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/MapFormInjector.java (revision 0) +++ resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/MapFormInjector.java (revision 0) @@ -0,0 +1,48 @@ +package org.jboss.resteasy.core; + +import java.lang.annotation.Annotation; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.regex.Pattern; +import org.jboss.resteasy.annotations.Form; +import org.jboss.resteasy.core.StringParameterInjector; +import org.jboss.resteasy.spi.ResteasyProviderFactory; + +/** + * Can inject maps. + */ +public class MapFormInjector extends AbstractCollectionFormInjector { + + private final StringParameterInjector keyInjector; + + /** + * Constructor. + */ + public MapFormInjector(Class collectionType, Class keyType, Class valueType, String prefix, ResteasyProviderFactory factory) { + super(collectionType, valueType, prefix, Pattern.compile("^" + prefix + "\\[([a-zA-Z_]+)\\]"), factory); + keyInjector = new StringParameterInjector(keyType, keyType, null, Form.class, null, null, new Annotation[0], factory); + } + + /** + * {@inheritDoc} + */ + @Override + protected Map createInstance(Class collectionType) { + if (collectionType.isAssignableFrom(LinkedHashMap.class)) { + return new LinkedHashMap(); + } + if (collectionType.isAssignableFrom(TreeMap.class)) { + return new TreeMap(); + } + throw new RuntimeException("Unsupported collectionType: " + collectionType); + } + + /** + * {@inheritDoc} + */ + @Override + protected void addTo(Map collection, String key, Object value) { + collection.put(keyInjector.extractValue(key), value); + } +} Index: resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/PrefixedMultivaluedMap.java =================================================================== --- resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/PrefixedMultivaluedMap.java (revision 0) +++ resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/PrefixedMultivaluedMap.java (revision 0) @@ -0,0 +1,48 @@ +package org.jboss.resteasy.util; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.ws.rs.core.MultivaluedMap; + +/** + * {@link MultivaluedMap} implementation that wraps another instance and only returns values that are prefixed with the given {@link #prefixWithDot}. + * The key type is assumed to be a {@link String}. + * @param The type of the values in the lists in the map. + */ +public class PrefixedMultivaluedMap extends DelegatingMultivaludMap { + + private final String prefixWithDot; + + /** + * Constructor setting the prefix and the delegate. + */ + public PrefixedMultivaluedMap(String prefix, MultivaluedMap delegate) { + super(delegate); + this.prefixWithDot = prefix + '.'; + } + + /** + * Returns the value assigned to "prefix.key" implicitly converts the key to {@link String}. + */ + @Override + public List get(Object key) { + return super.get(prefixWithDot + key); + } + + /** + * Filters the keySet. + */ + @Override + public Set keySet() { + HashSet result = new HashSet(); + for (String key : super.keySet()) { + if (key.startsWith(prefixWithDot)) { + result.add(key.substring(prefixWithDot.length())); + } + } + return result; + } + +} Index: resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/form/CollectionsFormTest.java =================================================================== --- resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/form/CollectionsFormTest.java (revision 0) +++ resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/form/CollectionsFormTest.java (revision 0) @@ -0,0 +1,72 @@ +package org.jboss.resteasy.test.form; + +import static junit.framework.Assert.*; + +import java.util.List; +import java.util.Map; + +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.annotations.Form; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.jboss.resteasy.test.BaseResourceTest; +import org.junit.Before; +import org.junit.Test; + +public class CollectionsFormTest extends BaseResourceTest { + + public static class Person { + @Form(prefix="telephoneNumbers") List telephoneNumbers; + @Form(prefix="address") Map adresses; + } + + public static class TelephoneNumber { + @FormParam("countryCode") private String countryCode; + @FormParam("number") private String number; + } + + public static class Address { + @FormParam("street") private String street; + @FormParam("houseNumber") private String houseNumber; + } + + @Path("person") + public static class MyResource { + + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public void post (@Form Person p) { + assertEquals(2, p.telephoneNumbers.size()); + assertEquals(2, p.adresses.size()); + assertEquals("31", p.telephoneNumbers.get(0).countryCode); + assertEquals("91", p.telephoneNumbers.get(1).countryCode); + assertEquals("Main Street", p.adresses.get("INVOICE").street); + assertEquals("Square One", p.adresses.get("SHIPPING").street); + } + } + + @Before + public void register () { + addPerRequestResource(MyResource.class); + } + + @Test + public void shouldSupportCollectionsInForm() throws Exception { + MockHttpResponse response = new MockHttpResponse(); + MockHttpRequest request = MockHttpRequest.post("person").accept(MediaType.TEXT_PLAIN).contentType(MediaType.APPLICATION_FORM_URLENCODED); + request.addFormHeader("telephoneNumbers[0].countryCode", "31"); + request.addFormHeader("telephoneNumbers[0].number", "0612345678"); + request.addFormHeader("telephoneNumbers[1].countryCode", "91"); + request.addFormHeader("telephoneNumbers[1].number", "9717738723"); + request.addFormHeader("address[INVOICE].street", "Main Street"); + request.addFormHeader("address[INVOICE].houseNumber", "2"); + request.addFormHeader("address[SHIPPING].street", "Square One"); + request.addFormHeader("address[SHIPPING].houseNumber", "13"); + dispatcher.invoke(request, response); + } +} Index: resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/PrefixedFormInjector.java =================================================================== --- resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/PrefixedFormInjector.java (revision 0) +++ resteasy-jaxrs/src/main/java/org/jboss/resteasy/core/PrefixedFormInjector.java (revision 0) @@ -0,0 +1,65 @@ +package org.jboss.resteasy.core; + +import java.util.List; + +import javax.ws.rs.core.MultivaluedMap; + +import org.jboss.resteasy.spi.HttpRequest; +import org.jboss.resteasy.spi.HttpResponse; +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.jboss.resteasy.util.PrefixedFormFieldsHttpRequest; + +/** + * Extension of {@link FormInjector} that handles prefixes for associated classes. + */ +public class PrefixedFormInjector extends FormInjector { + + private final String prefix; + + /** + * Constructor setting the prefix. + */ + public PrefixedFormInjector(Class type, String prefix, ResteasyProviderFactory factory) { + super(type, factory); + this.prefix = prefix; + } + + /** + * {@inheritDoc} Wraps the request in a + */ + @Override + public Object inject(HttpRequest request, HttpResponse response) { + if (!containsPrefixedFormFieldsWithValue(request.getDecodedFormParameters())) { + return null; + } + return doInject(prefix, request, response); + } + + /** + * Calls the super {@link #inject(HttpRequest, HttpResponse)} method. + */ + protected Object doInject(String prefix, HttpRequest request, HttpResponse response) { + return super.inject(new PrefixedFormFieldsHttpRequest(prefix, request), response); + } + + /** + * Checks to see if the decodedParameters contains any form fields starting with the prefix. Also checks if the value is not empty. + */ + private boolean containsPrefixedFormFieldsWithValue(MultivaluedMap decodedFormParameters) { + for (String parameterName : decodedFormParameters.keySet()) { + if (parameterName.startsWith(prefix)) { + if (hasValue(decodedFormParameters.get(parameterName))) { + return true; + } + } + } + return false; + } + + /** + * Checks that the list has an non empty value. + */ + protected boolean hasValue(List list) { + return !list.isEmpty() && list.get(0).length() > 0; + } +} Index: resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/form/NestedCollectionsFormTest.java =================================================================== --- resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/form/NestedCollectionsFormTest.java (revision 0) +++ resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/form/NestedCollectionsFormTest.java (revision 0) @@ -0,0 +1,81 @@ +package org.jboss.resteasy.test.form; + +import static junit.framework.Assert.*; + +import java.util.List; +import java.util.Map; + +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.annotations.Form; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.jboss.resteasy.test.BaseResourceTest; +import org.junit.Before; +import org.junit.Test; + +public class NestedCollectionsFormTest extends BaseResourceTest { + + public static class Person { + @Form(prefix="telephoneNumbers") List telephoneNumbers; + @Form(prefix="address") Map adresses; + } + + public static class TelephoneNumber { + @Form(prefix="country") private Country country; + @FormParam("number") private String number; + } + + public static class Address { + @FormParam("street") private String street; + @FormParam("houseNumber") private String houseNumber; + @Form(prefix="country") private Country country; + } + + public static class Country { + @FormParam("code") private String code; + } + + @Path("person") + public static class MyResource { + + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public void post (@Form Person p) { + assertEquals(2, p.telephoneNumbers.size()); + assertEquals(2, p.adresses.size()); + assertEquals("31", p.telephoneNumbers.get(0).country.code); + assertEquals("91", p.telephoneNumbers.get(1).country.code); + assertEquals("Main Street", p.adresses.get("INVOICE").street); + assertEquals("NL", p.adresses.get("INVOICE").country.code); + assertEquals("Square One", p.adresses.get("SHIPPING").street); + assertEquals("IN", p.adresses.get("SHIPPING").country.code); + } + } + + @Before + public void register () { + addPerRequestResource(MyResource.class); + } + + @Test + public void shouldSupportCollectionsWithNestedObjectsInForm() throws Exception { + MockHttpResponse response = new MockHttpResponse(); + MockHttpRequest request = MockHttpRequest.post("person").accept(MediaType.TEXT_PLAIN).contentType(MediaType.APPLICATION_FORM_URLENCODED); + request.addFormHeader("telephoneNumbers[0].country.code", "31"); + request.addFormHeader("telephoneNumbers[0].number", "0612345678"); + request.addFormHeader("telephoneNumbers[1].country.code", "91"); + request.addFormHeader("telephoneNumbers[1].number", "9717738723"); + request.addFormHeader("address[INVOICE].street", "Main Street"); + request.addFormHeader("address[INVOICE].houseNumber", "2"); + request.addFormHeader("address[INVOICE].country.code", "NL"); + request.addFormHeader("address[SHIPPING].street", "Square One"); + request.addFormHeader("address[SHIPPING].houseNumber", "13"); + request.addFormHeader("address[SHIPPING].country.code", "IN"); + dispatcher.invoke(request, response); + } +} Index: resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/PrefixedFormFieldsHttpRequest.java =================================================================== --- resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/PrefixedFormFieldsHttpRequest.java (revision 0) +++ resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/PrefixedFormFieldsHttpRequest.java (revision 0) @@ -0,0 +1,21 @@ +package org.jboss.resteasy.util; + +import javax.ws.rs.core.MultivaluedMap; + +import org.jboss.resteasy.spi.HttpRequest; + +public class PrefixedFormFieldsHttpRequest extends DelegatingHttpRequest { + + private final String prefix; + + public PrefixedFormFieldsHttpRequest(String prefix, HttpRequest request) { + super(request); + this.prefix = prefix; + } + + @Override + public MultivaluedMap getDecodedFormParameters() { + return new PrefixedMultivaluedMap(prefix, super.getDecodedFormParameters()); + } + +}