From 9d48cc56f15b4efeeeeb7c5d2fda6b144d160d90 Mon Sep 17 00:00:00 2001 From: Jeff Stano Date: Fri, 22 Aug 2014 08:46:47 -0600 Subject: [PATCH 1/3] #229: add support for curies to the HalEmbeddedBuilder class --- .../hateoas/hal/HalEmbeddedBuilder.java | 22 ++++++++---- .../hateoas/hal/Jackson2HalModule.java | 36 ++++++++++--------- .../hal/HalEmbeddedBuilderUnitTest.java | 11 +++--- 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java b/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java index f269ac9e0..6a2b8339a 100644 --- a/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java +++ b/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; +import org.springframework.hateoas.Link; import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.Resource; import org.springframework.hateoas.core.EmbeddedWrapper; @@ -32,7 +33,7 @@ /** * Builder class that allows collecting objects under the relation types defined for the objects but moving from the * single resource relation to the collection one, once more than one object of the same type is added. - * + * * @author Oliver Gierke * @author Dietrich Schulten */ @@ -42,27 +43,29 @@ class HalEmbeddedBuilder { private final Map embeddeds = new HashMap(); private final RelProvider provider; + private final CurieProvider curieProvider; private final EmbeddedWrappers wrappers; /** * Creates a new {@link HalEmbeddedBuilder} using the given {@link RelProvider} and prefer collection rels flag. - * + * * @param provider can be {@literal null}. * @param preferCollectionRels whether to prefer to ask the provider for collection rels. */ - public HalEmbeddedBuilder(RelProvider provider, boolean preferCollectionRels) { + public HalEmbeddedBuilder(RelProvider provider, CurieProvider curieProvider, boolean preferCollectionRels) { Assert.notNull(provider, "Relprovider must not be null!"); this.provider = provider; + this.curieProvider = curieProvider; this.wrappers = new EmbeddedWrappers(preferCollectionRels); } /** * Adds the given value to the embeddeds. Will skip doing so if the value is {@literal null} or the content of a * {@link Resource} is {@literal null}. - * - * @param value can be {@literal null}. + * + * @param source can be {@literal null}. */ public void add(Object source) { @@ -116,12 +119,19 @@ private String getDefaultedRelFor(EmbeddedWrapper wrapper, boolean forCollection Class type = wrapper.getRelTargetType(); String rel = forCollection ? provider.getCollectionResourceRelFor(type) : provider.getItemResourceRelFor(type); + + if (curieProvider != null) { + Link embeddedLinkRel = new Link("http://localhost", rel); + + rel = curieProvider.getNamespacedRelFrom(embeddedLinkRel); + } + return rel == null ? DEFAULT_REL : rel; } /** * Returns the added objects keyed up by their relation types. - * + * * @return */ public Map asMap() { diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java index 468ea8f58..fc8a5dfdc 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java @@ -71,7 +71,7 @@ /** * Jackson 2 module implementation to render {@link Link} and {@link ResourceSupport} instances in HAL compatible JSON. - * + * * @author Alexander Baetz * @author Oliver Gierke */ @@ -90,7 +90,7 @@ public Jackson2HalModule() { /** * Returns whether the module was already registered in the given {@link ObjectMapper}. - * + * * @param mapper must not be {@literal null}. * @return */ @@ -102,7 +102,7 @@ public static boolean isAlreadyRegisteredIn(ObjectMapper mapper) { /** * Custom {@link JsonSerializer} to render Link instances in HAL compatible JSON. - * + * * @author Alexander Baetz * @author Oliver Gierke */ @@ -230,7 +230,7 @@ protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { /** * Custom {@link JsonSerializer} to render {@link Resource}-Lists in HAL compatible JSON. Renders the list as a Map. - * + * * @author Alexander Baetz * @author Oliver Gierke */ @@ -238,24 +238,26 @@ public static class HalResourcesSerializer extends ContainerSerializer value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException { - HalEmbeddedBuilder builder = new HalEmbeddedBuilder(relProvider, enforceEmbeddedCollections); + HalEmbeddedBuilder builder = new HalEmbeddedBuilder(relProvider, curieProvider, enforceEmbeddedCollections); for (Object resource : value) { builder.add(resource); @@ -275,7 +277,7 @@ public void serialize(Collection value, JsonGenerator jgen, SerializerProvide @Override public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { - return new HalResourcesSerializer(property, relProvider, enforceEmbeddedCollections); + return new HalResourcesSerializer(property, relProvider, curieProvider, enforceEmbeddedCollections); } @Override @@ -307,7 +309,7 @@ protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { /** * Custom {@link JsonSerializer} to render Link instances in HAL compatible JSON. Renders the {@link Link} as * immediate object if we have a single one or as array if we have multiple ones. - * + * * @author Alexander Baetz * @author Oliver Gierke */ @@ -323,7 +325,7 @@ public OptionalListJackson2Serializer() { /** * Creates a new {@link OptionalListJackson2Serializer} using the given {@link BeanProperty}. - * + * * @param property */ public OptionalListJackson2Serializer(BeanProperty property) { @@ -412,7 +414,7 @@ public JavaType getContentType() { /* * (non-Javadoc) - * + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#hasSingleElement(java.lang.Object) */ @Override @@ -422,7 +424,7 @@ public boolean hasSingleElement(Object arg0) { /* * (non-Javadoc) - * + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#isEmpty(java.lang.Object) */ @Override @@ -432,7 +434,7 @@ public boolean isEmpty(Object arg0) { /* * (non-Javadoc) - * + * * @see com.fasterxml.jackson.databind.ser.ContextualSerializer#createContextual(com.fasterxml.jackson.databind.SerializerProvider, * com.fasterxml.jackson.databind.BeanProperty) */ @@ -601,7 +603,7 @@ public HalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider, Assert.notNull(resolver, "RelProvider must not be null!"); this.instanceMap.put(HalResourcesSerializer.class, new HalResourcesSerializer(resolver, - enforceEmbeddedCollections)); + curieProvider, enforceEmbeddedCollections)); this.instanceMap.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider)); } @@ -662,7 +664,7 @@ public TypeIdResolver typeIdResolverInstance(MapperConfig config, Annotated a /** * {@link JsonSerializer} to only render {@link Boolean} values if they're set to {@literal true}. - * + * * @author Oliver Gierke * @since 0.9 */ diff --git a/src/test/java/org/springframework/hateoas/hal/HalEmbeddedBuilderUnitTest.java b/src/test/java/org/springframework/hateoas/hal/HalEmbeddedBuilderUnitTest.java index 2d7d3187b..852010a14 100644 --- a/src/test/java/org/springframework/hateoas/hal/HalEmbeddedBuilderUnitTest.java +++ b/src/test/java/org/springframework/hateoas/hal/HalEmbeddedBuilderUnitTest.java @@ -30,13 +30,14 @@ /** * Unit tests for {@link HalEmbeddedBuilder}. - * + * * @author Oliver Gierke * @author Dietrich Schulten */ public class HalEmbeddedBuilderUnitTest { RelProvider provider; + CurieProvider curieProvider; @Before public void setUp() { @@ -81,7 +82,7 @@ public void correctlyPilesUpResourcesInCollectionRel() { @Test public void forcesCollectionRelToBeUsedIfConfigured() { - HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider, true); + HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider, curieProvider, true); builder.add("Sample"); assertThat(builder.asMap().get("string"), is(nullValue())); @@ -96,7 +97,7 @@ public void doesNotPreferCollectionsIfRelAwareWasAdded() { EmbeddedWrappers wrappers = new EmbeddedWrappers(false); - HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider, true); + HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider, curieProvider, true); builder.add(wrappers.wrap("MyValue", "foo")); assertThat(builder.asMap().get("foo"), is(instanceOf(String.class))); @@ -107,7 +108,7 @@ public void doesNotPreferCollectionsIfRelAwareWasAdded() { */ @Test(expected = IllegalArgumentException.class) public void rejectsNullRelProvider() { - new HalEmbeddedBuilder(null, false); + new HalEmbeddedBuilder(null, curieProvider, false); } private static void assertHasValues(Map source, String rel, Object... values) { @@ -120,7 +121,7 @@ private static void assertHasValues(Map source, String rel, Obje private Map setUpBuilder(Object... values) { - HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider, false); + HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider, curieProvider, false); for (Object value : values) { builder.add(value); From d69d7126bf5890b9a607ba69facdfb09008e5d02 Mon Sep 17 00:00:00 2001 From: Jeff Stano Date: Fri, 22 Aug 2014 13:23:52 -0600 Subject: [PATCH 2/3] #229: add support for curies to the HalEmbeddedBuilder class --- .../hateoas/hal/CurieProvider.java | 9 +++ .../hateoas/hal/DefaultCurieProvider.java | 10 +++- .../hateoas/hal/HalEmbeddedBuilder.java | 8 +-- .../hal/DefaultCurieProviderUnitTest.java | 15 +++++ .../hal/DefaultCurieProviderUnitTests.java | 15 +++++ .../hal/HalEmbeddedBuilderUnitTest.java | 55 ++++++++++++++++--- 6 files changed, 99 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/springframework/hateoas/hal/CurieProvider.java b/src/main/java/org/springframework/hateoas/hal/CurieProvider.java index 36905b9db..6d3d6e4fc 100644 --- a/src/main/java/org/springframework/hateoas/hal/CurieProvider.java +++ b/src/main/java/org/springframework/hateoas/hal/CurieProvider.java @@ -38,6 +38,15 @@ public interface CurieProvider { */ String getNamespacedRelFrom(Link link); + /** + * Returns the rel to be rendered for the given rel. Will potentially prefix the rel but also might decide + * not to, depending on the actual rel. + * + * @param rel + * @return + */ + String getNamespacedRelFrom(String rel); + /** * Returns an object to render as the base curie information. Implementations have to make sure, the retunred * instances renders as defined in the spec. diff --git a/src/main/java/org/springframework/hateoas/hal/DefaultCurieProvider.java b/src/main/java/org/springframework/hateoas/hal/DefaultCurieProvider.java index 39393752d..f5221d70f 100644 --- a/src/main/java/org/springframework/hateoas/hal/DefaultCurieProvider.java +++ b/src/main/java/org/springframework/hateoas/hal/DefaultCurieProvider.java @@ -66,7 +66,15 @@ public Collection getCurieInformation(Links links) { @Override public String getNamespacedRelFrom(Link link) { - String rel = link.getRel(); + return getNamespacedRelFrom(link.getRel()); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.hal.CurieProvider#getNamespacedRelFrom(java.lang.String) + */ + @Override + public String getNamespacedRelFrom(String rel) { boolean prefixingNeeded = !IanaRels.isIanaRel(rel) && !rel.contains(":"); return prefixingNeeded ? String.format("%s:%s", curie.name, rel) : rel; diff --git a/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java b/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java index 6a2b8339a..5ed0a4180 100644 --- a/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java +++ b/src/main/java/org/springframework/hateoas/hal/HalEmbeddedBuilder.java @@ -120,11 +120,9 @@ private String getDefaultedRelFor(EmbeddedWrapper wrapper, boolean forCollection String rel = forCollection ? provider.getCollectionResourceRelFor(type) : provider.getItemResourceRelFor(type); - if (curieProvider != null) { - Link embeddedLinkRel = new Link("http://localhost", rel); - - rel = curieProvider.getNamespacedRelFrom(embeddedLinkRel); - } + if (curieProvider != null) { + rel = curieProvider.getNamespacedRelFrom(rel); + } return rel == null ? DEFAULT_REL : rel; } diff --git a/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTest.java b/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTest.java index 8dbfc3d0b..029ebc12c 100644 --- a/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTest.java +++ b/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTest.java @@ -72,4 +72,19 @@ public void prefixesNormalRels() { public void doesNotPrefixQualifiedRels() { assertThat(provider.getNamespacedRelFrom(new Link("http://amazon.com", "custom:rel")), is("custom:rel")); } + + @Test + public void doesNotPrefixIanaRelsForRelAsString() { + assertThat(provider.getNamespacedRelFrom("self"), is("self")); + } + + @Test + public void prefixesNormalRelsForRelAsString() { + assertThat(provider.getNamespacedRelFrom("book"), is("acme:book")); + } + + @Test + public void doesNotPrefixQualifiedRelsForRelAsString() { + assertThat(provider.getNamespacedRelFrom("custom:rel"), is("custom:rel")); + } } diff --git a/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTests.java b/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTests.java index 4f99af640..99bb41834 100644 --- a/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTests.java +++ b/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTests.java @@ -72,4 +72,19 @@ public void prefixesNormalRels() { public void doesNotPrefixQualifiedRels() { assertThat(provider.getNamespacedRelFrom(new Link("http://amazon.com", "custom:rel")), is("custom:rel")); } + + @Test + public void doesNotPrefixIanaRelsForRelAsString() { + assertThat(provider.getNamespacedRelFrom("self"), is("self")); + } + + @Test + public void prefixesNormalRelsForRelAsString() { + assertThat(provider.getNamespacedRelFrom("book"), is("acme:book")); + } + + @Test + public void doesNotPrefixQualifiedRelsForRelAsString() { + assertThat(provider.getNamespacedRelFrom("custom:rel"), is("custom:rel")); + } } diff --git a/src/test/java/org/springframework/hateoas/hal/HalEmbeddedBuilderUnitTest.java b/src/test/java/org/springframework/hateoas/hal/HalEmbeddedBuilderUnitTest.java index 852010a14..70ae5c365 100644 --- a/src/test/java/org/springframework/hateoas/hal/HalEmbeddedBuilderUnitTest.java +++ b/src/test/java/org/springframework/hateoas/hal/HalEmbeddedBuilderUnitTest.java @@ -25,6 +25,7 @@ import org.junit.Before; import org.junit.Test; import org.springframework.hateoas.RelProvider; +import org.springframework.hateoas.UriTemplate; import org.springframework.hateoas.core.EmbeddedWrappers; import org.springframework.hateoas.core.EvoInflectorRelProvider; @@ -42,53 +43,93 @@ public class HalEmbeddedBuilderUnitTest { @Before public void setUp() { provider = new EvoInflectorRelProvider(); + curieProvider = new DefaultCurieProvider("curie", new UriTemplate("http://localhost/{rel}")); } @Test public void rendersSingleElementsWithSingleEntityRel() { - Map map = setUpBuilder("foo", 1L); + Map map = setUpBuilder(null, "foo", 1L); assertThat(map.get("string"), is((Object) "foo")); assertThat(map.get("long"), is((Object) 1L)); } + @Test + public void rendersSingleElementsWithSingleEntityRelWithCurieProvider() { + + Map map = setUpBuilder(curieProvider, "foo", 1L); + + assertThat(map.get("curie:string"), is((Object) "foo")); + assertThat(map.get("curie:long"), is((Object) 1L)); + } + @Test public void rendersMultipleElementsWithCollectionResourceRel() { - Map map = setUpBuilder("foo", "bar", 1L); + Map map = setUpBuilder(null, "foo", "bar", 1L); assertThat(map.containsKey("string"), is(false)); assertThat(map.get("long"), is((Object) 1L)); assertHasValues(map, "strings", "foo", "bar"); } + @Test + public void rendersMultipleElementsWithCollectionResourceRelWithCurieProvider() { + + Map map = setUpBuilder(curieProvider, "foo", "bar", 1L); + + assertThat(map.containsKey("curie:string"), is(false)); + assertThat(map.get("curie:long"), is((Object) 1L)); + assertHasValues(map, "curie:strings", "foo", "bar"); + } + /** * @see #110 */ @Test public void correctlyPilesUpResourcesInCollectionRel() { - Map map = setUpBuilder("foo", "bar", "foobar", 1L); + Map map = setUpBuilder(null, "foo", "bar", "foobar", 1L); assertThat(map.containsKey("string"), is(false)); assertHasValues(map, "strings", "foo", "bar", "foobar"); assertThat(map.get("long"), is((Object) 1L)); } + @Test + public void correctlyPilesUpResourcesInCollectionRelWithCurieprovider() { + + Map map = setUpBuilder(curieProvider, "foo", "bar", "foobar", 1L); + + assertThat(map.containsKey("curie:string"), is(false)); + assertHasValues(map, "curie:strings", "foo", "bar", "foobar"); + assertThat(map.get("curie:long"), is((Object) 1L)); + } + /** * @see #135 */ @Test public void forcesCollectionRelToBeUsedIfConfigured() { - HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider, curieProvider, true); + HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider, null, true); builder.add("Sample"); assertThat(builder.asMap().get("string"), is(nullValue())); assertHasValues(builder.asMap(), "strings", "Sample"); } + @Test + public void forcesCollectionRelToBeUsedIfConfiguredWithCurieProvider() { + + HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider, curieProvider, true); + builder.add("Sample"); + + assertThat(builder.asMap().get("curie:string"), is(nullValue())); + assertHasValues(builder.asMap(), "curie:strings", "Sample"); + } + /** * @see #195 */ @@ -97,7 +138,7 @@ public void doesNotPreferCollectionsIfRelAwareWasAdded() { EmbeddedWrappers wrappers = new EmbeddedWrappers(false); - HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider, curieProvider, true); + HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider, null, true); builder.add(wrappers.wrap("MyValue", "foo")); assertThat(builder.asMap().get("foo"), is(instanceOf(String.class))); @@ -108,7 +149,7 @@ public void doesNotPreferCollectionsIfRelAwareWasAdded() { */ @Test(expected = IllegalArgumentException.class) public void rejectsNullRelProvider() { - new HalEmbeddedBuilder(null, curieProvider, false); + new HalEmbeddedBuilder(null, null, false); } private static void assertHasValues(Map source, String rel, Object... values) { @@ -119,7 +160,7 @@ private static void assertHasValues(Map source, String rel, Obje assertThat((List) value, Matchers.> allOf(hasSize(values.length), hasItems(values))); } - private Map setUpBuilder(Object... values) { + private Map setUpBuilder(CurieProvider curieProvider, Object... values) { HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider, curieProvider, false); From 619be4f86cfe79a51d43db9770107633e1817785 Mon Sep 17 00:00:00 2001 From: Jeff Stano Date: Tue, 26 Aug 2014 08:32:47 -0600 Subject: [PATCH 3/3] #235: modified the Link class to support adding target attributes --- .../org/springframework/hateoas/Link.java | 186 +++++++++++++++++- .../springframework/hateoas/LinkUnitTest.java | 46 +++++ .../hal/Jackson2HalIntegrationTest.java | 14 ++ 3 files changed, 240 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/springframework/hateoas/Link.java b/src/main/java/org/springframework/hateoas/Link.java index 95457ae1c..8d014574e 100755 --- a/src/main/java/org/springframework/hateoas/Link.java +++ b/src/main/java/org/springframework/hateoas/Link.java @@ -16,10 +16,13 @@ package org.springframework.hateoas; import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -27,6 +30,8 @@ import javax.xml.bind.annotation.XmlTransient; import javax.xml.bind.annotation.XmlType; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonInclude; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -55,6 +60,7 @@ public class Link implements Serializable { @XmlAttribute private String rel; @XmlAttribute private String href; @XmlTransient @JsonIgnore private UriTemplate template; + private Map attributes = new TreeMap(); /** * Creates a new link to the given URI with the self rel. @@ -99,6 +105,17 @@ protected Link() { } + /** + * Copy constructor needed for the various with methods. + */ + private Link(Link linkToCopy) { + + this.template = linkToCopy.template; + this.href = linkToCopy.href; + this.rel = linkToCopy.rel; + this.attributes = linkToCopy.attributes; + } + /** * Returns the actual URI the link is pointing to. * @@ -117,6 +134,18 @@ public String getRel() { return rel; } + /** + * Returns the attributes of the link. + * + * @return + */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonAnyGetter + public Map getAttributes() { + + return Collections.unmodifiableMap(attributes); + } + /** * Returns a {@link Link} pointing to the same URI but with the given relation. * @@ -136,6 +165,83 @@ public Link withSelfRel() { return withRel(Link.REL_SELF); } + /** + * Returns a {@link Link} pointing to the same URI but with the specified anchor value. + * + * @param anchor + * @return + */ + public Link withAnchor(String anchor) { + + Link link = new Link(this); + link.setAttributeValue("anchor", anchor); + return link; + } + + /** + * Returns a {@link Link} pointing to the same URI but with the specified hreflang value. + * + * @param hreflang + * @return + */ + public Link withHreflang(String hreflang) { + + return withAttribute("hreflang", hreflang); + } + + /** + * Returns a {@link Link} pointing to the same URI but with the specified media value. + * + * @param media + * @return + */ + public Link withMedia(String media) { + + Link link = new Link(this); + link.setAttributeValue("media", media); + return link; + } + + /** + * Returns a {@link Link} pointing to the same URI but with the specified title value. + * + * @param title + * @return + */ + public Link withTitle(String title) { + + Link link = new Link(this); + link.setAttributeValue("title", title); + return link; + } + + /** + * Returns a {@link Link} pointing to the same URI but with the specified type value. + * + * @param type + * @return + */ + public Link withType(String type) { + + Link link = new Link(this); + link.setAttributeValue("type", type); + return link; + } + + /** + * Returns a {@link Link} pointing to the same URI but with the specified attribute + * + * @param key + * @param value + * @return + */ + public Link withAttribute(String key, String value) { + + Link link = new Link(this); + link.setAttributeValue(key, value); + return link; + } + /** * Returns the variable names contained in the template. * @@ -233,7 +339,59 @@ public int hashCode() { */ @Override public String toString() { - return String.format("<%s>;rel=\"%s\"", href, rel); + + StringBuilder str = new StringBuilder(); + + str.append("<"); + str.append(href); + str.append(">"); + + if (rel != null) { + str.append(";rel=\""); + str.append(rel); + str.append("\""); + } + + for (String key : attributes.keySet()) { + Object value = attributes.get(key); + + if (value instanceof Collection) { + for (String item : (Collection)value) { + str.append(";"); + str.append(key); + str.append("=\""); + str.append(item); + str.append("\""); + } + } + else { + str.append(";"); + str.append(key); + str.append("=\""); + str.append(value); + str.append("\""); + } + } + + return str.toString(); + } + + private void setAttributeValue(String key, String value) { + + Object currentValue = attributes.get(key); + + if (currentValue instanceof Collection) { + ((Collection)currentValue).add(value); + } + else if (currentValue instanceof String) { + Collection values = new ArrayList(); + attributes.put(key, values); + values.add(currentValue.toString()); + values.add(value); + } + else { + attributes.put(key, value); + } } /** @@ -262,7 +420,15 @@ public static Link valueOf(String element) { throw new IllegalArgumentException("Link does not provide a rel attribute!"); } - return new Link(matcher.group(1), attributes.get("rel")); + Link link = new Link(matcher.group(1), attributes.get("rel")); + + for (String key : attributes.keySet()) { + if (!key.equalsIgnoreCase("rel")) { + link = link.withAttribute(key, attributes.get(key)); + } + } + + return link; } else { throw new IllegalArgumentException(String.format("Given link header %s is not RFC5988 compliant!", element)); @@ -282,11 +448,19 @@ private static Map getAttributeMap(String source) { } Map attributes = new HashMap(); - Pattern keyAndValue = Pattern.compile("(\\w+)=\\\"(\\p{Alnum}*)\""); - Matcher matcher = keyAndValue.matcher(source); - while (matcher.find()) { - attributes.put(matcher.group(1), matcher.group(2)); + Pattern attributesPattern = Pattern.compile("\\w+=\\\"[\\s\\p{Alnum}]*\"*"); + Pattern keyAndValuePattern = Pattern.compile("(\\w+)=\\\"([\\s\\p{Alnum}]*)\""); + + Matcher attributesMatcher = attributesPattern.matcher(source); + + while (attributesMatcher.find()) { + String group = attributesMatcher.group(); + Matcher keyValueMatcher = keyAndValuePattern.matcher(group); + + if (keyValueMatcher.find()) { + attributes.put(keyValueMatcher.group(1), keyValueMatcher.group(2)); + } } return attributes; diff --git a/src/test/java/org/springframework/hateoas/LinkUnitTest.java b/src/test/java/org/springframework/hateoas/LinkUnitTest.java index 99abac041..0af33e5e3 100644 --- a/src/test/java/org/springframework/hateoas/LinkUnitTest.java +++ b/src/test/java/org/springframework/hateoas/LinkUnitTest.java @@ -44,6 +44,26 @@ public void createsLinkFromRelAndHref() { assertThat(link.getRel(), is(Link.REL_SELF)); } + @Test + public void createsLinkFromRelAndHrefWithParameters() { + + Link link = new Link("foo", Link.REL_SELF) + .withAnchor("anchor") + .withHreflang("hreflang") + .withMedia("media") + .withTitle("title") + .withType("type") + .withAttribute("name", "name"); + assertThat(link.getHref(), is("foo")); + assertThat(link.getRel(), is(Link.REL_SELF)); + assertThat(link.getAttributes().get("anchor").toString(), is("anchor")); + assertThat(link.getAttributes().get("hreflang").toString(), is("hreflang")); + assertThat(link.getAttributes().get("media").toString(), is("media")); + assertThat(link.getAttributes().get("title").toString(), is("title")); + assertThat(link.getAttributes().get("type").toString(), is("type")); + assertThat(link.getAttributes().get("name").toString(), is("name")); + } + @Test(expected = IllegalArgumentException.class) public void rejectsNullHref() { new Link(null); @@ -108,6 +128,32 @@ public void parsesRFC5988HeaderIntoLink() { assertThat(Link.valueOf(";rel=\"foo\""), is(new Link("/something", "foo"))); assertThat(Link.valueOf(";rel=\"foo\";title=\"Some title\""), is(new Link("/something", "foo"))); + assertThat(Link.valueOf(";title=\"Some title\";rel=\"foo\""), is(new Link("/something", "foo"))); + assertThat(Link.valueOf(";rel=\"foo\";title=\"Some title\"").getAttributes().get("title").toString(), is("Some title")); + } + + @Test + public void testToStringWithNoAttributes() { + + Link link = new Link("/foo", Link.REL_SELF); + + assertThat(link.toString(), is(";rel=\"self\"")); + } + + @Test + public void testToStringWithAllAttributes() { + + Link link = new Link("/foo", Link.REL_SELF) + .withAnchor("anchor") + .withHreflang("hreflang") + .withMedia("media") + .withTitle("title") + .withType("type") + .withAttribute("name", "name") + .withAttribute("custom", "custom1") + .withAttribute("custom", "custom2"); + + assertThat(link.toString(), is(";rel=\"self\";anchor=\"anchor\";custom=\"custom1\";custom=\"custom2\";hreflang=\"hreflang\";media=\"media\";name=\"name\";title=\"title\";type=\"type\"")); } @Test(expected = IllegalArgumentException.class) diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java index af459a2ef..df8cc619b 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java @@ -51,6 +51,8 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg static final String SINGLE_LINK_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}}}"; static final String LIST_LINK_REFERENCE = "{\"_links\":{\"self\":[{\"href\":\"localhost\"},{\"href\":\"localhost2\"}]}}"; + static final String SINGLE_LINK_WITH_ATTRIBUTES_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\",\"hreflang\":[\"lang1\",\"lang2\"],\"title\":\"The Title\"}}}"; + static final String SIMPLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[\"first\",\"second\"]}}"; static final String SINGLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}"; static final String LIST_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}"; @@ -95,6 +97,18 @@ public void deserializeSingleLink() throws Exception { assertThat(read(SINGLE_LINK_REFERENCE, ResourceSupport.class), is(expected)); } + @Test + public void rendersSingleLinkWithAttributesAsObject() throws Exception { + + ResourceSupport resourceSupport = new ResourceSupport(); + resourceSupport.add(new Link("localhost") + .withTitle("The Title") + .withHreflang("lang1") + .withHreflang("lang2")); + + assertThat(write(resourceSupport), is(SINGLE_LINK_WITH_ATTRIBUTES_REFERENCE)); + } + /** * @see #29 */