Skip to content

Add type and title properties to Link class #79

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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
160 changes: 136 additions & 24 deletions src/main/java/org/springframework/hateoas/Link.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import javax.xml.bind.annotation.XmlType;

import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
Expand All @@ -37,9 +38,14 @@
public class Link implements Serializable {

private static final long serialVersionUID = -9037755944661782121L;
private static final String ATOM_LINK_SPEC_URL = "http://tools.ietf.org/html/rfc4287#section-4.2.7";
private static Pattern ATOM_MEDIA_TYPE_PATTERN = Pattern.compile(".+/.+");
// private static final String ATOM_LINK_HREF = "href";
private static final String ATOM_LINK_REL = "rel";
private static final String ATOM_LINK_TITLE = "title";
private static final String ATOM_LINK_TYPE = "type";

public static final String ATOM_NAMESPACE = "http://www.w3.org/2005/Atom";

public static final String REL_SELF = "self";
public static final String REL_FIRST = "first";
public static final String REL_PREVIOUS = "prev";
Expand All @@ -50,12 +56,17 @@ public class Link implements Serializable {
private String rel;
@XmlAttribute
private String href;
@XmlAttribute
private String title;
@XmlAttribute
private String type;

/**
* Creates a new link to the given URI with the self rel.
*
* @see #REL_SELF
* @param href must not be {@literal null} or empty.
* @param href
* must not be {@literal null} or empty.
*/
public Link(String href) {
this(href, REL_SELF);
Expand All @@ -64,16 +75,49 @@ public Link(String href) {
/**
* Creates a new {@link Link} to the given URI with the given rel.
*
* @param href must not be {@literal null} or empty.
* @param rel must not be {@literal null} or empty.
* @param href
* must not be {@literal null} or empty.
* @param rel
* must not be {@literal null} or empty.
*/
public Link(String href, String rel) {
this(href, rel, null, null);
}

/**
* Creates a new {@link Link} to the given URI with the given rel.
*
* @param href
* must not be {@literal null} or empty.
* @param rel
* must not be {@literal null} or empty.
* @param title
* may be {@literal null} or empty.
* @param type
* may be {@literal null} or empty.
*/
public Link(String href, String rel, String title, String type) {

Assert.hasText(href, "Href must not be null or empty!");
Assert.hasText(rel, "Rel must not be null or empty!");
Assert.isTrue((title == null) || StringUtils.hasText(title), "Title must not be empty!");
Assert.isTrue((type == null) || isAtomMediaType(type), "Type must be valid atom media type! (see " + ATOM_LINK_SPEC_URL + ")");

this.href = href;
this.rel = rel;
this.title = title;
this.type = type;
}

/**
* returns check whether passed string is valid atom media type per
* {@value #ATOM_LINK_SPEC_URL}
*
* @param type
* @return
*/
private boolean isAtomMediaType(String type) {
return ATOM_MEDIA_TYPE_PATTERN.matcher(type).matches();
}

/**
Expand Down Expand Up @@ -102,26 +146,71 @@ public String getRel() {
}

/**
* Returns a {@link Link} pointing to the same URI but with the given relation.
* Returns the title of the link (may be null)
*
* @return
*/
public String getTitle() {
return title;
}

/**
* Returns the type of the link (may be null)
*
* @return
*/
public String getType() {
return type;
}

/**
* Returns a {@link Link} pointing to the same URI but with the given
* relation.
*
* @param rel must not be {@literal null} or empty.
* @param rel
* must not be {@literal null} or empty.
* @return
*/
public Link withRel(String rel) {
return new Link(href, rel);
}

/**
* Returns a {@link Link} pointing to the same URI but with the {@code self} relation.
* Returns a {@link Link} pointing to the same URI but with the {@code self}
* relation.
*
* @return
*/
public Link withSelfRel() {
return withRel(Link.REL_SELF);
}

/*
/**
* Returns a {@link Link} based on current Link, but with given title
*
* @param title
* may be {@literal null} or non-empty.
* @return
*/
public Link withTitle(String title) {
return new Link(href, rel, title, type);
}

/**
* Returns a {@link Link} based on current Link, but with given title
*
* @param type
* may be {@literal null} or valid atom media type per
* {@value #ATOM_LINK_SPEC_URL}
* @return
*/
public Link withType(String type) {
return new Link(href, rel, title, type);
}

/*
* (non-Javadoc)
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
Expand All @@ -137,38 +226,55 @@ public boolean equals(Object obj) {

Link that = (Link) obj;

return this.href.equals(that.href) && this.rel.equals(that.rel);
return this.href.equals(that.href) && this.rel.equals(that.rel) && ObjectUtils.nullSafeEquals(this.title, that.title)
&& ObjectUtils.nullSafeEquals(this.type, that.type);
}

/*
/*
* (non-Javadoc)
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {

int result = 17;
result += 31 * href.hashCode();
result += 31 * rel.hashCode();
result += 31 * ObjectUtils.nullSafeHashCode(title);
result += 31 * ObjectUtils.nullSafeHashCode(type);
return result;
}

/*
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return String.format("<%s>;rel=\"%s\"", href, rel);
String result = String.format("<%s>;%s=\"%s\"", href, ATOM_LINK_REL, rel);
if (title != null) {
result += String.format("%s;%s=\"%s\"", result, ATOM_LINK_TITLE, title);
}
if (type != null) {
result += String.format("%s;%s=\"%s\"", result, ATOM_LINK_TYPE, type);
}
return result;
}

/**
* Factory method to easily create {@link Link} instances from RFC-5988 compatible {@link String} representations of a
* link. Will return {@literal null} if an empty or {@literal null} {@link String} is given.
* Factory method to easily create {@link Link} instances from RFC-5988
* compatible {@link String} representations of a link. Will return
* {@literal null} if an empty or {@literal null} {@link String} is given.
*
* @param element an RFC-5899 compatible representation of a link.
* @throws IllegalArgumentException if a non-empty {@link String} was given that does not adhere to RFC-5899.
* @throws IllegalArgumentException if no {@code rel} attribute could be found.
* @param element
* an RFC-5988 compatible representation of a link.
* @throws IllegalArgumentException
* if a non-empty {@link String} was given that does not adhere
* to RFC-5988.
* @throws IllegalArgumentException
* if no {@code rel} attribute could be found.
* @return
*/
public static Link valueOf(String element) {
Expand All @@ -184,11 +290,11 @@ public static Link valueOf(String element) {

Map<String, String> attributes = getAttributeMap(matcher.group(2));

if (!attributes.containsKey("rel")) {
if (!attributes.containsKey(ATOM_LINK_REL)) {
throw new IllegalArgumentException("Link does not provide a rel attribute!");
}

return new Link(matcher.group(1), attributes.get("rel"));
return new Link(matcher.group(1), attributes.get(ATOM_LINK_REL), attributes.get(ATOM_LINK_TITLE), attributes.get(ATOM_LINK_TYPE));

} else {
throw new IllegalArgumentException(String.format("Given link header %s is not RFC5988 compliant!", element));
Expand All @@ -208,11 +314,17 @@ 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);
Pattern keyAndValue = Pattern.compile("(\\w+)=\\\"(\\p{Print}*)\"");
String[] keyAndValues = source.split(";");
for (int i = 0; i < keyAndValues.length; i++) {

Matcher matcher = keyAndValue.matcher(keyAndValues[i]);

while (matcher.find()) {
attributes.put(matcher.group(1), matcher.group(2));
if (matcher.find()) {
attributes.put(matcher.group(1), matcher.group(2));
} else {
throw new RuntimeException(String.format("unexpected token found parsing link attributes [%s]", keyAndValues[i]));
}
}

return attributes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import org.junit.Before;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
Expand All @@ -20,6 +21,7 @@ public abstract class AbstractJackson2MarshallingIntegrationTests {
@Before
public void setUp() {
mapper = new ObjectMapper();
mapper.setSerializationInclusion(Include.NON_NULL);
}

protected String write(Object object) throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.io.Writer;

import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.junit.Before;

/**
Expand All @@ -33,6 +34,8 @@ public abstract class AbstractMarshallingIntegrationTests {
@Before
public void setUp() {
mapper = new ObjectMapper();
mapper.configure(SerializationConfig.Feature.WRITE_NULL_PROPERTIES, false);
//mapper.getSerializationConfig().withSerializationInclusion(JsonSerialize.Inclusion.NON_EMPTY);
}

protected String write(Object object) throws Exception {
Expand Down
9 changes: 8 additions & 1 deletion src/test/java/org/springframework/hateoas/LinkUnitTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,14 @@ public void returnsNullForNullOrEmptyLink() {
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>;rel=\"foo\";title=\"Some title\""), is(new Link("/something", "foo", "Some title", null)));
assertThat(Link.valueOf("</something>;rel=\"foo\";title=\"Some title\";type=\"application/json\""), is(new Link("/something", "foo",
"Some title", "application/json")));
}

@Test(expected = IllegalArgumentException.class)
public void disallowsInvalidAtomMediaType() {
new Link("/foo-href", "foo-rel", "foo-title", "foo-type");
}

@Test(expected = IllegalArgumentException.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
*/
package org.springframework.hateoas;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

import java.io.FileInputStream;
import java.io.IOException;
Expand All @@ -30,6 +30,7 @@
import javax.xml.bind.Marshaller;

import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.map.SerializationConfig.Feature;
import org.junit.Before;
import org.junit.Test;
Expand All @@ -38,6 +39,7 @@
import org.springframework.hateoas.hal.Jackson1HalModule;
import org.springframework.hateoas.hal.Jackson2HalModule;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.SerializationFeature;

/**
Expand All @@ -61,17 +63,21 @@ public VndErrorsMarshallingTests() throws IOException {
xmlReference = readFile(new ClassPathResource("vnderror.xml"));
}

@SuppressWarnings("deprecation")
@Before
public void setUp() throws Exception {

jackson1Mapper = new ObjectMapper();
jackson1Mapper.registerModule(new Jackson1HalModule());
jackson1Mapper.configure(Feature.INDENT_OUTPUT, true);

//jackson1Mapper.getSerializationConfig().withSerializationInclusion(JsonSerialize.Inclusion.NON_EMPTY);
jackson1Mapper.configure(SerializationConfig.Feature.WRITE_NULL_PROPERTIES, false);

jackson2Mapper = new com.fasterxml.jackson.databind.ObjectMapper();
jackson2Mapper.registerModule(new Jackson2HalModule());
jackson2Mapper.configure(SerializationFeature.INDENT_OUTPUT, true);

jackson2Mapper.setSerializationInclusion(Include.NON_NULL);

JAXBContext context = JAXBContext.newInstance(VndErrors.class);
marshaller = context.createMarshaller();

Expand Down