diff --git a/src/main/java/org/springframework/hateoas/Link.java b/src/main/java/org/springframework/hateoas/Link.java index d77ba757..6cd925ec 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 the original author or authors. + * Copyright 2012-2013 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. @@ -16,11 +16,17 @@ package org.springframework.hateoas; import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlType; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Value object for links. @@ -115,7 +121,8 @@ public class Link implements Serializable { return this.href.equals(that.href) && this.rel.equals(that.rel); } - /* (non-Javadoc) + /* + * (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override @@ -133,6 +140,62 @@ public class Link implements Serializable { */ @Override public String toString() { - return String.format("{ rel : %s, href : %s }", rel, href); + return String.format("<%s>;rel=\"%s\"", href, rel); + } + + /** + * 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. + * @return + */ + public static Link valueOf(String element) { + + if (!StringUtils.hasText(element)) { + return null; + } + + Pattern uriAndAttributes = Pattern.compile("<(.*)>;(.*)"); + Matcher matcher = uriAndAttributes.matcher(element); + + if (matcher.find()) { + + Map attributes = getAttributeMap(matcher.group(2)); + + if (!attributes.containsKey("rel")) { + throw new IllegalArgumentException("Link does not provide a rel attribute!"); + } + + return new Link(matcher.group(1), attributes.get("rel")); + + } else { + throw new IllegalArgumentException(String.format("Given link header %s is not RFC5988 compliant!", element)); + } + } + + /** + * Parses the links attributes from the given source {@link String}. + * + * @param source + * @return + */ + private static Map getAttributeMap(String source) { + + if (!StringUtils.hasText(source)) { + return Collections.emptyMap(); + } + + 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)); + } + + return attributes; } } diff --git a/src/main/java/org/springframework/hateoas/Links.java b/src/main/java/org/springframework/hateoas/Links.java new file mode 100644 index 00000000..51a27c07 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/Links.java @@ -0,0 +1,163 @@ +/* + * Copyright 2013 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.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.springframework.util.StringUtils; + +/** + * Value object to represent a list of {@link Link}s. + * + * @author Oliver Gierke + */ +public class Links implements Iterable { + + static final Links NO_LINKS = new Links(Collections. emptyList()); + + private final List links; + + /** + * Creates a new {@link Links} instance from the given {@link Link}s. + * + * @param links + */ + public Links(List links) { + this.links = links == null ? Collections. emptyList() : Collections.unmodifiableList(links); + } + + /** + * Creates a new {@link Links} instance from the given {@link Link}s. + * + * @param links + */ + public Links(Link... links) { + this(Arrays.asList(links)); + } + + /** + * Returns the {@link Link} with the given rel. + * + * @param rel the relation type to lookup a link for. + * @return the {@link Link} with the given rel or {@literal null} if none found. + */ + public Link getLink(String rel) { + + for (Link link : links) { + if (link.getRel().equals(rel)) { + return link; + } + } + + return null; + } + + /** + * Returns all {@link Links} with the given relation type. + * + * @return the links + */ + public List getLinks(String rel) { + + List result = new ArrayList(); + + for (Link link : links) { + if (link.getRel().endsWith(rel)) { + result.add(link); + } + } + + return result; + } + + /** + * Creates a {@link Links} instance from the given RFC5988-compatible link format. + * + * @param source a comma separated list of {@link Link} representations. + * @return the {@link Links} represented by the given {@link String}. + */ + public static Links valueOf(String source) { + + if (!StringUtils.hasText(source)) { + return NO_LINKS; + } + + String[] elements = source.split(","); + List links = new ArrayList(elements.length); + + for (String element : elements) { + + Link link = Link.valueOf(element); + + if (link != null) { + links.add(link); + } + } + + return new Links(links); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return StringUtils.collectionToCommaDelimitedString(links); + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @Override + public Iterator iterator() { + return links.iterator(); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object arg0) { + + if (!(arg0 instanceof Links)) { + return false; + } + + Links that = (Links) arg0; + + return this.links.equals(that.links); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = 17; + result += 31 * links.hashCode(); + + return result; + } +} diff --git a/src/test/java/org/springframework/hateoas/LinkUnitTest.java b/src/test/java/org/springframework/hateoas/LinkUnitTest.java index 4a4d8a5a..1adfc2d0 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 the original author or authors. + * Copyright 2012-2013 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. @@ -91,4 +91,33 @@ public class LinkUnitTest { public void differentTypeDoesNotEqual() { assertThat(new Link("foo"), is(not((Object) new ResourceSupport()))); } + + @Test + public void returnsNullForNullOrEmptyLink() { + + assertThat(Link.valueOf(null), is(nullValue())); + assertThat(Link.valueOf(""), is(nullValue())); + } + + @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"))); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsMissingRelAttribute() { + Link.valueOf(");title=\"title\""); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsLinkWithoutAttributesAtAll() { + Link.valueOf(");title=\"title\""); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNonRFC5988String() { + Link.valueOf("foo"); + } } diff --git a/src/test/java/org/springframework/hateoas/LinksUnitTest.java b/src/test/java/org/springframework/hateoas/LinksUnitTest.java new file mode 100644 index 00000000..7c6edadd --- /dev/null +++ b/src/test/java/org/springframework/hateoas/LinksUnitTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013 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 static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.util.Arrays; + +import org.junit.Test; +import org.springframework.util.StringUtils; + +/** + * Unit test for {@link Links}. + * + * @author Oliver Gierke + */ +public class LinksUnitTest { + + static final String FIRST = ";rel=\"foo\""; + static final String SECOND = ";rel=\"bar\""; + + static final String LINKS = StringUtils.collectionToCommaDelimitedString(Arrays.asList(FIRST, SECOND)); + + static final Links reference = new Links(new Link("/something", "foo"), new Link("/somethingElse", "bar")); + + @Test + public void parsesLinkHeaderLinks() { + + assertThat(Links.valueOf(LINKS), is(reference)); + assertThat(reference.toString(), is(LINKS)); + } + + @Test + public void skipsEmptyLinkElements() { + assertThat(Links.valueOf(LINKS + ",,,"), is(reference)); + } + + @Test + public void returnsNullForNullOrEmptySource() { + + assertThat(Links.valueOf(null), is(Links.NO_LINKS)); + assertThat(Links.valueOf(""), is(Links.NO_LINKS)); + } + + @Test + public void getSingleLinkByRel() { + assertThat(reference.getLink("bar"), is(new Link("/somethingElse", "bar"))); + } +}