diff --git a/src/main/java/org/springframework/hateoas/TemplateVariable.java b/src/main/java/org/springframework/hateoas/TemplateVariable.java index 2411fb61..7541ee95 100644 --- a/src/main/java/org/springframework/hateoas/TemplateVariable.java +++ b/src/main/java/org/springframework/hateoas/TemplateVariable.java @@ -31,6 +31,7 @@ import org.springframework.util.StringUtils; * A single template variable. * * @author Oliver Gierke + * @author JamesE Richardson */ @Value @EqualsAndHashCode @@ -150,14 +151,15 @@ public final class TemplateVariable implements Serializable { REQUEST_PARAM("?", true), // REQUEST_PARAM_CONTINUED("&", true), // SEGMENT("/", true), // - FRAGMENT("#", true); + FRAGMENT("#", true), // + COMPOSITE_PARAM("*", true); private static final List COMBINABLE_TYPES = Arrays.asList(REQUEST_PARAM, REQUEST_PARAM_CONTINUED); private final String key; private final boolean optional; - private VariableType(String key, boolean optional) { + VariableType(String key, boolean optional) { this.key = key; this.optional = optional; diff --git a/src/main/java/org/springframework/hateoas/UriTemplate.java b/src/main/java/org/springframework/hateoas/UriTemplate.java index e1a65101..ab50f502 100644 --- a/src/main/java/org/springframework/hateoas/UriTemplate.java +++ b/src/main/java/org/springframework/hateoas/UriTemplate.java @@ -22,6 +22,8 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -36,12 +38,13 @@ import org.springframework.web.util.UriComponentsBuilder; * Custom URI template to support qualified URI template variables. * * @author Oliver Gierke + * @author JamesE Richardson * @see http://tools.ietf.org/html/rfc6570 * @since 0.9 */ public class UriTemplate implements Iterable, Serializable { - private static final Pattern VARIABLE_REGEX = Pattern.compile("\\{([\\?\\&#/]?)([\\w\\,]+)\\}"); + private static final Pattern VARIABLE_REGEX = Pattern.compile("\\{([\\?\\&#/]?)([\\w\\,*]+)\\}"); private static final long serialVersionUID = -1007874653930162262L; private final TemplateVariables variables; @@ -68,7 +71,14 @@ public class UriTemplate implements Iterable, Serializable { String[] names = matcher.group(2).split(","); for (String name : names) { - TemplateVariable variable = new TemplateVariable(name, type); + + TemplateVariable variable; + + if (name.endsWith(VariableType.COMPOSITE_PARAM.toString())) { + variable = new TemplateVariable(name.substring(0, name.length() - 1), VariableType.COMPOSITE_PARAM); + } else { + variable = new TemplateVariable(name, type); + } if (!variable.isRequired() && start < baseUriEndIndex) { baseUriEndIndex = start; @@ -276,6 +286,9 @@ public class UriTemplate implements Iterable, Serializable { } switch (variable.getType()) { + case COMPOSITE_PARAM: + appendComposite(builder, variable.getName(), value); + break; case REQUEST_PARAM: case REQUEST_PARAM_CONTINUED: builder.queryParam(variable.getName(), value); @@ -289,4 +302,27 @@ public class UriTemplate implements Iterable, Serializable { break; } } + + /** + * Expand what could be a single value, a {@link List}, or a {@link Map}. + * + * @param builder + * @param name + * @param value + * @see https://tools.ietf.org/html/rfc6570#section-2.4.2 + */ + private static void appendComposite(UriComponentsBuilder builder, String name, Object value) { + + if (value instanceof Iterable) { + for (Object valuePart : (Iterable) value) { + builder.queryParam(name, valuePart); + } + } else if (value instanceof Map) { + for (Entry queryParam : (Set>) ((Map) value).entrySet()) { + builder.queryParam(queryParam.getKey(), queryParam.getValue()); + } + } else { + builder.queryParam(name, value); + } + } } diff --git a/src/test/java/org/springframework/hateoas/UriTemplateUnitTest.java b/src/test/java/org/springframework/hateoas/UriTemplateUnitTest.java index 068112d4..62746923 100755 --- a/src/test/java/org/springframework/hateoas/UriTemplateUnitTest.java +++ b/src/test/java/org/springframework/hateoas/UriTemplateUnitTest.java @@ -33,6 +33,7 @@ import org.springframework.hateoas.TemplateVariable.VariableType; * Unit tests for {@link UriTemplate}. * * @author Oliver Gierke + * @author JamesE Richardson */ public class UriTemplateUnitTest { @@ -261,6 +262,70 @@ public class UriTemplateUnitTest { assertThat(template.toString()).isEqualTo("/{?q}"); } + /** + * @see #483 + */ + @Test + public void compositveValuesAreRecognisedAsVariableType() { + + UriTemplate template = new UriTemplate("/foo{&bar,foobar*}"); + + assertVariables(template, + new TemplateVariable("bar", VariableType.REQUEST_PARAM_CONTINUED), + new TemplateVariable("foobar", VariableType.COMPOSITE_PARAM)); + } + + /** + * @see #483 + */ + @Test + public void expandsCompositeValueAsAssociativeArray() { + + UriTemplate template = new UriTemplate("/foo{&bar,foobar*}"); + + String expandedTemplate = template.expand(new HashMap(){{ + put("bar", "barExpanded"); + put("foobar", new HashMap(){{ + put("city", "Clarksville"); + put("state", "TN"); + }}); + }}).toString(); + + assertThat(expandedTemplate).isEqualTo("/foo?bar=barExpanded&city=Clarksville&state=TN"); + } + + /** + * @see #483 + */ + @Test + public void expandsCompositeValueAsList() { + + UriTemplate template = new UriTemplate("/foo{&bar,foobar*}"); + + String expandedTemplate = template.expand(new HashMap(){{ + put("bar", "barExpanded"); + put("foobar", Arrays.asList("foo1", "foo2")); + }}).toString(); + + assertThat(expandedTemplate).isEqualTo("/foo?bar=barExpanded&foobar=foo1&foobar=foo2"); + } + + /** + * @see #483 + */ + @Test + public void handlesCompositeValueAsSingleValue() { + + UriTemplate template = new UriTemplate("/foo{&bar,foobar*}"); + + String expandedTemplate = template.expand(new HashMap(){{ + put("bar", "barExpanded"); + put("foobar", "singleValue"); + }}).toString(); + + assertThat(expandedTemplate).isEqualTo("/foo?bar=barExpanded&foobar=singleValue"); + } + private static void assertVariables(UriTemplate template, TemplateVariable... variables) { assertVariables(template, Arrays.asList(variables)); }