Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Modify the Link class to support RFC 5988 Target Attributes #238

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
#235: modified the Link class to support adding target attributes
  • Loading branch information
Jeff Stano committed Aug 26, 2014
commit 619be4f86cfe79a51d43db9770107633e1817785
186 changes: 180 additions & 6 deletions src/main/java/org/springframework/hateoas/Link.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,22 @@
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;

import javax.xml.bind.annotation.XmlAttribute;
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;

Expand Down Expand Up @@ -55,6 +60,7 @@ public class Link implements Serializable {
@XmlAttribute private String rel;
@XmlAttribute private String href;
@XmlTransient @JsonIgnore private UriTemplate template;
private Map<String,Object> attributes = new TreeMap<String, Object>();

/**
* Creates a new link to the given URI with the self rel.
Expand Down Expand Up @@ -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.
*
Expand All @@ -117,6 +134,18 @@ public String getRel() {
return rel;
}

/**
* Returns the attributes of the link.
*
* @return
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonAnyGetter
public Map<String,Object> getAttributes() {

return Collections.unmodifiableMap(attributes);
}

/**
* Returns a {@link Link} pointing to the same URI but with the given relation.
*
Expand All @@ -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.
*
Expand Down Expand Up @@ -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<String>)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<String>)currentValue).add(value);
}
else if (currentValue instanceof String) {
Collection<String> values = new ArrayList<String>();
attributes.put(key, values);
values.add(currentValue.toString());
values.add(value);
}
else {
attributes.put(key, value);
}
}

/**
Expand Down Expand Up @@ -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));
Expand All @@ -282,11 +448,19 @@ private static Map<String, String> getAttributeMap(String source) {
}

Map<String, String> attributes = new HashMap<String, String>();
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;
Expand Down
46 changes: 46 additions & 0 deletions src/test/java/org/springframework/hateoas/LinkUnitTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -108,6 +128,32 @@ public void parsesRFC5988HeaderIntoLink() {

assertThat(Link.valueOf("</something>;rel=\"foo\""), is(new Link("/something", "foo")));
assertThat(Link.valueOf("</something>;rel=\"foo\";title=\"Some title\""), is(new Link("/something", "foo")));
assertThat(Link.valueOf("</something>;title=\"Some title\";rel=\"foo\""), is(new Link("/something", "foo")));
assertThat(Link.valueOf("</something>;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("</foo>;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("</foo>;rel=\"self\";anchor=\"anchor\";custom=\"custom1\";custom=\"custom2\";hreflang=\"hreflang\";media=\"media\";name=\"name\";title=\"title\";type=\"type\""));
}

@Test(expected = IllegalArgumentException.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\"}}}]}}";
Expand Down Expand Up @@ -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
*/
Expand Down