diff --git a/src/main/java/org/springframework/hateoas/Link.java b/src/main/java/org/springframework/hateoas/Link.java index 11ac811d6..97e3c014b 100755 --- a/src/main/java/org/springframework/hateoas/Link.java +++ b/src/main/java/org/springframework/hateoas/Link.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,13 @@ */ package org.springframework.hateoas; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.Wither; + import java.io.Serializable; import java.util.Collections; import java.util.HashMap; @@ -37,9 +44,14 @@ * Value object for links. * * @author Oliver Gierke + * @author Greg Turnquist */ @XmlType(name = "link", namespace = Link.ATOM_NAMESPACE) @JsonIgnoreProperties("templated") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PACKAGE) +@Getter +@EqualsAndHashCode(of = {"rel", "href", "hreflang", "media", "title", "deprecation"}) public class Link implements Serializable { private static final long serialVersionUID = -9037755944661782121L; @@ -53,8 +65,13 @@ public class Link implements Serializable { public static final String REL_NEXT = "next"; public static final String REL_LAST = "last"; - @XmlAttribute private String rel; - @XmlAttribute private String href; + @XmlAttribute @Wither private String rel; + @XmlAttribute @Wither private String href; + @XmlAttribute @Wither private String hreflang; + @XmlAttribute @Wither private String media; + @XmlAttribute @Wither private String title; + @XmlAttribute @Wither private String type; + @XmlAttribute @Wither private String deprecation; @XmlTransient @JsonIgnore private UriTemplate template; /** @@ -85,7 +102,7 @@ public Link(String href, String rel) { */ public Link(UriTemplate template, String rel) { - Assert.notNull(template, "UriTempalte must not be null!"); + Assert.notNull(template, "UriTemplate must not be null!"); Assert.hasText(rel, "Rel must not be null or empty!"); this.template = template; @@ -93,41 +110,6 @@ public Link(UriTemplate template, String rel) { this.rel = rel; } - /** - * Empty constructor required by the marshalling framework. - */ - protected Link() { - - } - - /** - * Returns the actual URI the link is pointing to. - * - * @return - */ - public String getHref() { - return href; - } - - /** - * Returns the rel of the link. - * - * @return - */ - public String getRel() { - return rel; - } - - /** - * Returns a {@link Link} pointing to the same URI but with the given relation. - * - * @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. * @@ -195,46 +177,35 @@ private UriTemplate getUriTemplate() { return template; } - /* + /* * (non-Javadoc) - * @see java.lang.Object#equals(java.lang.Object) + * @see java.lang.Object#toString() */ @Override - public boolean equals(Object obj) { + public String toString() { + String linkString = String.format("<%s>;rel=\"%s\"", href, rel); - if (this == obj) { - return true; + if (hreflang != null) { + linkString += ";hreflang=\"" + hreflang + "\""; } - if (!(obj instanceof Link)) { - return false; + if (media != null) { + linkString += ";media=\"" + media + "\""; } - Link that = (Link) obj; - - return this.href.equals(that.href) && this.rel.equals(that.rel); - } - - /* - * (non-Javadoc) - * @see java.lang.Object#hashCode() - */ - @Override - public int hashCode() { + if (title != null) { + linkString += ";title=\"" + title + "\""; + } - int result = 17; - result += 31 * href.hashCode(); - result += 31 * rel.hashCode(); - return result; - } + if (type != null) { + linkString += ";type=\"" + type + "\""; + } - /* - * (non-Javadoc) - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - return String.format("<%s>;rel=\"%s\"", href, rel); + if (deprecation != null) { + linkString += ";deprecation=\"" + deprecation + "\""; + } + + return linkString; } /** @@ -263,7 +234,29 @@ 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")); + + if (attributes.containsKey("hreflang")) { + link = link.withHreflang(attributes.get("hreflang")); + } + + if (attributes.containsKey("media")) { + link = link.withMedia(attributes.get("media")); + } + + if (attributes.containsKey("title")) { + link = link.withTitle(attributes.get("title")); + } + + if (attributes.containsKey("type")) { + link = link.withType(attributes.get("type")); + } + + if (attributes.containsKey("deprecation")) { + link = link.withDeprecation(attributes.get("deprecation")); + } + + return link; } else { throw new IllegalArgumentException(String.format("Given link header %s is not RFC5988 compliant!", element)); @@ -283,7 +276,7 @@ private static Map getAttributeMap(String source) { } Map attributes = new HashMap(); - Pattern keyAndValue = Pattern.compile("(\\w+)=\"(\\p{Lower}[\\p{Lower}\\p{Digit}\\.\\-]*|" + URI_PATTERN + ")\""); + Pattern keyAndValue = Pattern.compile("(\\w+)=\"(\\p{Lower}[\\p{Lower}\\p{Digit}\\.\\-\\s]*|" + URI_PATTERN + ")\""); Matcher matcher = keyAndValue.matcher(source); while (matcher.find()) { diff --git a/src/main/java/org/springframework/hateoas/Links.java b/src/main/java/org/springframework/hateoas/Links.java index f58554b46..bf8b6903d 100644 --- a/src/main/java/org/springframework/hateoas/Links.java +++ b/src/main/java/org/springframework/hateoas/Links.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2016 the original author or authors. + * Copyright 2013-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,10 +29,11 @@ * Value object to represent a list of {@link Link}s. * * @author Oliver Gierke + * @author Greg Turnquist */ public class Links implements Iterable { - private static final Pattern LINK_HEADER_PATTERN = Pattern.compile("(<[^>]*>;rel=\"[^\"]*\")"); + private static final Pattern LINK_HEADER_PATTERN = Pattern.compile("(<[^>]*>(;\\w+=\"[^\"]*\")+)"); static final Links NO_LINKS = new Links(Collections. emptyList()); diff --git a/src/main/java/org/springframework/hateoas/hal/LinkMixin.java b/src/main/java/org/springframework/hateoas/hal/LinkMixin.java index 53f796eb9..b7b9293c4 100644 --- a/src/main/java/org/springframework/hateoas/hal/LinkMixin.java +++ b/src/main/java/org/springframework/hateoas/hal/LinkMixin.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,13 +28,46 @@ * * @author Alexander Baetz * @author Oliver Gierke + * @author Greg Turnquist */ -@JsonIgnoreProperties(value = "rel") +@JsonIgnoreProperties({"rel", "media"}) abstract class LinkMixin extends Link { private static final long serialVersionUID = 4720588561299667409L; - /* + /* + * (non-Javadoc) + * @see org.springframework.hateoas.Link#getHreflang() + */ + @Override + @JsonInclude(Include.NON_NULL) + public abstract String getHreflang(); + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.Link#getTitle() + */ + @Override + @JsonInclude(Include.NON_NULL) + public abstract String getTitle(); + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.Link#getType() + */ + @Override + @JsonInclude(Include.NON_NULL) + public abstract String getType(); + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.Link#getDeprecation() + */ + @Override + @JsonInclude(Include.NON_NULL) + public abstract String getDeprecation(); + + /* * (non-Javadoc) * @see org.springframework.hateoas.Link#isTemplate() */ diff --git a/src/test/java/org/springframework/hateoas/AbstractJackson2MarshallingIntegrationTest.java b/src/test/java/org/springframework/hateoas/AbstractJackson2MarshallingIntegrationTest.java index df0133d6d..49723e6a0 100644 --- a/src/test/java/org/springframework/hateoas/AbstractJackson2MarshallingIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/AbstractJackson2MarshallingIntegrationTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.hateoas; import java.io.StringWriter; @@ -5,6 +20,7 @@ import org.junit.Before; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; /** @@ -12,6 +28,7 @@ * * @author Oliver Gierke * @author Jon Brisbin + * @author Greg Turnquist */ public abstract class AbstractJackson2MarshallingIntegrationTest { @@ -20,6 +37,7 @@ public abstract class AbstractJackson2MarshallingIntegrationTest { @Before public void setUp() { mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); } protected String write(Object object) throws Exception { diff --git a/src/test/java/org/springframework/hateoas/LinkUnitTest.java b/src/test/java/org/springframework/hateoas/LinkUnitTest.java index 727822f2a..03c1efffb 100644 --- a/src/test/java/org/springframework/hateoas/LinkUnitTest.java +++ b/src/test/java/org/springframework/hateoas/LinkUnitTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ * Unit tests for {@link Link}. * * @author Oliver Gierke + * @author Greg Turnquist */ public class LinkUnitTest { @@ -103,21 +104,46 @@ public void returnsNullForNullOrEmptyLink() { assertThat(Link.valueOf(""), is(nullValue())); } + /** + * @see #54 + * @see #100 + */ @Test 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(";rel=\"self\";hreflang=\"en\";media=\"pdf\";title=\"pdf customer copy\";type=\"portable document\";deprecation=\"http://example.com/customers/deprecated\""), + is(new Link("/customer/1") + .withHreflang("en") + .withMedia("pdf") + .withTitle("pdf customer copy") + .withType("portable document") + .withDeprecation("http://example.com/customers/deprecated"))); + } + + /** + * @see #100 + */ + @Test + public void ignoresUnrecognizedAttributes() { + Link link = Link.valueOf(";rel=\"foo\";unknown=\"should fail\""); + + assertThat(link.getHref(), is("/something")); + assertThat(link.getRel(), is("foo")); } @Test(expected = IllegalArgumentException.class) public void rejectsMissingRelAttribute() { - Link.valueOf(");title=\"title\""); + Link.valueOf(";title=\"title\""); } @Test(expected = IllegalArgumentException.class) public void rejectsLinkWithoutAttributesAtAll() { - Link.valueOf(");title=\"title\""); + + Link link = Link.valueOf(""); + + System.out.println(link); } @Test(expected = IllegalArgumentException.class) diff --git a/src/test/java/org/springframework/hateoas/LinksUnitTest.java b/src/test/java/org/springframework/hateoas/LinksUnitTest.java index b16004afa..a9dba4649 100644 --- a/src/test/java/org/springframework/hateoas/LinksUnitTest.java +++ b/src/test/java/org/springframework/hateoas/LinksUnitTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2016 the original author or authors. + * Copyright 2013-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ * Unit test for {@link Links}. * * @author Oliver Gierke + * @author Greg Turnquist */ public class LinksUnitTest { @@ -36,18 +37,27 @@ public class LinksUnitTest { static final String LINKS = StringUtils.collectionToCommaDelimitedString(Arrays.asList(FIRST, SECOND)); - static final Links reference = new Links(new Link("/something", "foo"), new Link("/somethingElse", "bar")); + static final String THIRD = ";rel=\"foo\";hreflang=\"en\""; + static final String FOURTH = ";rel=\"bar\";hreflang=\"de\""; + + static final String LINKS2 = StringUtils.collectionToCommaDelimitedString(Arrays.asList(THIRD, FOURTH)); + static final Links reference = new Links(new Link("/something", "foo"), new Link("/somethingElse", "bar")); + static final Links reference2 = new Links(new Link("/something", "foo").withHreflang("en"), new Link("/somethingElse", "bar").withHreflang("de")); + @Test public void parsesLinkHeaderLinks() { assertThat(Links.valueOf(LINKS), is(reference)); + assertThat(Links.valueOf(LINKS2), is(reference2)); assertThat(reference.toString(), is(LINKS)); + assertThat(reference2.toString(), is(LINKS2)); } @Test public void skipsEmptyLinkElements() { assertThat(Links.valueOf(LINKS + ",,,"), is(reference)); + assertThat(Links.valueOf(LINKS2 + ",,,"), is(reference2)); } @Test @@ -57,9 +67,14 @@ public void returnsNullForNullOrEmptySource() { assertThat(Links.valueOf(""), is(Links.NO_LINKS)); } + /** + * @see #54 + * @see #100 + */ @Test public void getSingleLinkByRel() { assertThat(reference.getLink("bar"), is(new Link("/somethingElse", "bar"))); + assertThat(reference2.getLink("bar"), is(new Link("/somethingElse", "bar").withHreflang("de"))); } /** diff --git a/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTest.java b/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTest.java index 5e20279dc..ed9327f91 100644 --- a/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTest.java +++ b/src/test/java/org/springframework/hateoas/hal/DefaultCurieProviderUnitTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2016 the original author or authors. + * Copyright 2013-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ * Unit tests for {@link DefaultCurieProvider}. * * @author Oliver Gierke + * @author Greg Turnquist */ public class DefaultCurieProviderUnitTest { @@ -82,6 +83,19 @@ public void doesNotPrefixQualifiedRels() { assertThat(provider.getNamespacedRelFrom(new Link("http://amazon.com", "custom:rel")), is("custom:rel")); } + /** + * @see #100 + */ + @Test + public void prefixesNormalRelsThatHaveExtraRFC5988Attributes() { + assertThat(provider.getNamespacedRelFrom(new Link("http://amazon.com", "custom:rel") + .withHreflang("en") + .withTitle("the title") + .withMedia("the media") + .withType("the type") + .withDeprecation("http://example.com/custom/deprecated")), is("custom:rel")); + } + /** * @see #229 */ diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java index df858c8df..25a4faa09 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.junit.Before; import org.junit.Test; + import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.support.MessageSourceAccessor; @@ -51,6 +52,7 @@ * * @author Alexander Baetz * @author Oliver Gierke + * @author Greg Turnquist */ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingIntegrationTest { @@ -77,6 +79,9 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg static final String LINK_WITH_TITLE = "{\"_links\":{\"ns:foobar\":{\"href\":\"target\",\"title\":\"Foobar's title!\"}}}"; + static final String SINGLE_WITH_ONE_EXTRA_ATTRIBUTES = "{\"_links\":{\"self\":{\"href\":\"localhost\",\"title\":\"the title\"}}}"; + static final String SINGLE_WITH_ALL_EXTRA_ATTRIBUTES = "{\"_links\":{\"self\":{\"href\":\"localhost\",\"hreflang\":\"en\",\"title\":\"the title\",\"type\":\"the type\",\"deprecation\":\"/customers/deprecated\"}}}"; + @Before public void setUpModule() { @@ -96,6 +101,33 @@ public void rendersSingleLinkAsObject() throws Exception { assertThat(write(resourceSupport), is(SINGLE_LINK_REFERENCE)); } + /** + * @see #100 + */ + @Test + public void rendersAllExtraRFC5988Attributes() throws Exception { + + ResourceSupport resourceSupport = new ResourceSupport(); + resourceSupport.add(new Link("localhost", "self") + .withHreflang("en") + .withTitle("the title") + .withType("the type") + .withMedia("the media") + .withDeprecation("/customers/deprecated")); + + assertThat(write(resourceSupport), is(SINGLE_WITH_ALL_EXTRA_ATTRIBUTES)); + } + + @Test + public void rendersWithOneExtraRFC5988Attribute() throws Exception { + + ResourceSupport resourceSupport = new ResourceSupport(); + resourceSupport.add(new Link("localhost", "self") + .withTitle("the title")); + + assertThat(write(resourceSupport), is(SINGLE_WITH_ONE_EXTRA_ATTRIBUTES)); + } + @Test public void deserializeSingleLink() throws Exception { ResourceSupport expected = new ResourceSupport();