diff --git a/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java b/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java index 0c995e30..74b98dd0 100644 --- a/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java +++ b/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 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. diff --git a/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java b/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java index e3d26418..f3f95d8e 100644 --- a/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java +++ b/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 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. @@ -19,7 +19,7 @@ package com.example.notes; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.springframework.restdocs.core.RestDocumentation.document; -import static org.springframework.restdocs.core.RestDocumentation.halLinks; +import static org.springframework.restdocs.core.LinkExtractors.halLinks; import static org.springframework.restdocs.core.RestDocumentation.linkWithRel; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; diff --git a/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java b/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java index 745c4bfe..da8ca724 100644 --- a/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java +++ b/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 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. @@ -18,8 +18,8 @@ package com.example.notes; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.restdocs.core.LinkExtractors.halLinks; import static org.springframework.restdocs.core.RestDocumentation.document; -import static org.springframework.restdocs.core.RestDocumentation.halLinks; import static org.springframework.restdocs.core.RestDocumentation.linkWithRel; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/Link.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/Link.java new file mode 100644 index 00000000..135a51e4 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/Link.java @@ -0,0 +1,94 @@ +/* + * Copyright 2014-2015 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.restdocs.core; + +import org.springframework.core.style.ToStringCreator; + +/** + * Representation of a link used in a Hypermedia-based API + * + * @author Andy Wilkinson + */ +public class Link { + + private final String rel; + + private final String href; + + /** + * Creates a new {@code Link} with the given {@code rel} and {@code href} + * + * @param rel The link's rel + * @param href The link's href + */ + public Link(String rel, String href) { + this.rel = rel; + this.href = href; + } + + /** + * Returns the link's {@code rel} + * @return the link's {@code rel} + */ + public String getRel() { + return rel; + } + + /** + * Returns the link's {@code href} + * @return the link's {@code href} + */ + public String getHref() { + return href; + } + + @Override + public int hashCode() { + int prime = 31; + int result = 1; + result = prime * result + href.hashCode(); + result = prime * result + rel.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Link other = (Link) obj; + if (!href.equals(other.href)) { + return false; + } + if (!rel.equals(other.rel)) { + return false; + } + return true; + } + + public String toString() { + return new ToStringCreator(this).append("rel", this.rel) + .append("href", this.href).toString(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractor.java index 3df9e7b8..bb65fd8e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 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,10 +16,30 @@ package org.springframework.restdocs.core; +import java.io.IOException; +import java.util.List; import java.util.Map; +import org.springframework.mock.web.MockHttpServletResponse; + +/** + * A {@code LinkExtractor} is used to extract {@link Link links} from a JSON response. The + * expected format of the links in the response is determined by the implementation. + * + * @author Andy Wilkinson + * + */ public interface LinkExtractor { - Map extractLinks(Map responseJson); + /** + * Extract the links from the given response, returning a {@code Map} of links where + * the keys are the link rels. + * + * @param response The response from which the links are to be extracted + * @return The extracted links, keyed by rel + * @throws IOException if link extraction fails + */ + Map> extractLinks(MockHttpServletResponse response) + throws IOException; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java new file mode 100644 index 00000000..e6c28d53 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java @@ -0,0 +1,162 @@ +/* + * Copyright 2014-2015 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.restdocs.core; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.mock.web.MockHttpServletResponse; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Static factory methods provided a selection of {@link LinkExtractor link extractors} + * for use when documentating a hypermedia-based API. + * + * @author Andy Wilkinson + * + */ +public class LinkExtractors { + + /** + * Returns a {@code LinkExtractor} capable of extracting links in Hypermedia + * Application Language (HAL) format where the links are found in a map named + * {@code _links}. + * + * @return The extract for HAL-style links + */ + public static LinkExtractor halLinks() { + return new HalLinkExtractor(); + } + + /** + * Returns a {@code LinkExtractor} capable of extracting links in Atom format where + * the links are found in an array named {@code links}. + * + * @return The extractor for Atom-style links + */ + public static LinkExtractor atomLinks() { + return new AtomLinkExtractor(); + } + + private static abstract class JsonContentLinkExtractor implements LinkExtractor { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @SuppressWarnings("unchecked") + public Map> extractLinks(MockHttpServletResponse response) + throws IOException { + Map jsonContent = this.objectMapper.readValue( + response.getContentAsString(), Map.class); + return extractLinks(jsonContent); + } + + protected abstract Map> extractLinks(Map json); + } + + private static class HalLinkExtractor extends JsonContentLinkExtractor { + + @SuppressWarnings("unchecked") + @Override + public Map> extractLinks(Map json) { + Map> extractedLinks = new HashMap<>(); + Object possibleLinks = json.get("_links"); + if (possibleLinks instanceof Map) { + Map links = (Map) possibleLinks; + for (Entry entry : links.entrySet()) { + String rel = (String) entry.getKey(); + extractedLinks.put(rel, convertToLinks(entry.getValue(), rel)); + } + } + return extractedLinks; + } + + @SuppressWarnings("unchecked") + private static List convertToLinks(Object object, String rel) { + List links = new ArrayList<>(); + if (object instanceof Collection) { + Collection hrefObjects = (Collection) object; + for (Object hrefObject : hrefObjects) { + maybeAddLink(maybeCreateLink(rel, hrefObject), links); + } + } + else { + maybeAddLink(maybeCreateLink(rel, object), links); + } + return links; + } + + private static Link maybeCreateLink(String rel, Object possibleHref) { + if (possibleHref instanceof String) { + return new Link(rel, (String) possibleHref); + } + return null; + } + + private static void maybeAddLink(Link possibleLink, List links) { + if (possibleLink != null) { + links.add(possibleLink); + } + } + } + + private static class AtomLinkExtractor extends JsonContentLinkExtractor { + + @SuppressWarnings("unchecked") + @Override + public Map> extractLinks(Map json) { + Map> extractedLinks = new HashMap<>(); + Object possibleLinks = json.get("links"); + if (possibleLinks instanceof Collection) { + Collection linksCollection = (Collection) possibleLinks; + for (Object linkObject : linksCollection) { + if (linkObject instanceof Map) { + Link link = maybeCreateLink((Map) linkObject); + maybeStoreLink(link, extractedLinks); + } + } + } + return extractedLinks; + } + + private static Link maybeCreateLink(Map linkMap) { + Object hrefObject = linkMap.get("href"); + Object relObject = linkMap.get("rel"); + if (relObject instanceof String && hrefObject instanceof String) { + return new Link((String) relObject, (String) hrefObject); + } + return null; + } + + private static void maybeStoreLink(Link link, + Map> extractedLinks) { + if (link != null) { + List linksForRel = extractedLinks.get(link.getRel()); + if (linksForRel == null) { + linksForRel = new ArrayList(); + extractedLinks.put(link.getRel(), linksForRel); + } + linksForRel.add(link); + } + } + } +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java index 160119ca..974e1dbb 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 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,8 +16,6 @@ package org.springframework.restdocs.core; -import java.util.Map; - public class RestDocumentation { public static RestDocumentationResultHandler document(String outputDir) @@ -29,14 +27,4 @@ public class RestDocumentation { return new LinkDescriptor(rel); } - public static LinkExtractor halLinks() { - return new LinkExtractor() { - - @SuppressWarnings("unchecked") - @Override - public Map extractLinks(Map responseJson) { - return (Map) responseJson.get("_links"); - } - }; - } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java index 04afb317..256279d0 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 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. @@ -41,8 +41,6 @@ import org.springframework.util.Assert; import org.springframework.util.FileCopyUtils; import org.springframework.web.bind.annotation.RequestMethod; -import com.fasterxml.jackson.databind.ObjectMapper; - public abstract class RestDocumentationResultHandlers { public static CurlResultHandler documentCurlRequest(String outputDir) { @@ -245,8 +243,6 @@ public abstract class RestDocumentationResultHandlers { static class LinkDocumentingResultHandler extends RestDocumentationResultHandler { - private final ObjectMapper objectMapper = new ObjectMapper(); - private final Map descriptorsByRel = new HashMap(); private final LinkExtractor extractor; @@ -262,12 +258,10 @@ public abstract class RestDocumentationResultHandlers { } } - @SuppressWarnings("unchecked") @Override void handle(MvcResult result, DocumentationWriter writer) throws Exception { - Map json = this.objectMapper.readValue(result.getResponse() - .getContentAsString(), Map.class); - Map links = this.extractor.extractLinks(json); + Map> links = this.extractor.extractLinks(result + .getResponse()); Set actualRels = links.keySet(); Set expectedRels = this.descriptorsByRel.keySet(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java new file mode 100644 index 00000000..2e94fead --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2014-2015 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.restdocs.core; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.FileCopyUtils; + +/** + * Tests for {@link LinkExtractors}. + * + * @author Andy Wilkinson + */ +@RunWith(Parameterized.class) +public class LinkExtractorsTests { + + private final LinkExtractor linkExtractor; + + private final String linkType; + + @Parameters + public static Collection data() { + return Arrays.asList(new Object[] { LinkExtractors.halLinks(), "hal" }, + new Object[] { LinkExtractors.atomLinks(), "atom" }); + } + + public LinkExtractorsTests(LinkExtractor linkExtractor, String linkType) { + this.linkExtractor = linkExtractor; + this.linkType = linkType; + } + + @Test + public void singleLink() throws IOException { + Map> links = this.linkExtractor + .extractLinks(createResponse("single-link")); + assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com")), links); + } + + @Test + public void multipleLinksWithDifferentRels() throws IOException { + Map> links = this.linkExtractor + .extractLinks(createResponse("multiple-links-different-rels")); + assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com"), + new Link("bravo", "http://bravo.example.com")), links); + } + + @Test + public void multipleLinksWithSameRels() throws IOException { + Map> links = this.linkExtractor + .extractLinks(createResponse("multiple-links-same-rels")); + assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com/one"), + new Link("alpha", "http://alpha.example.com/two")), links); + } + + @Test + public void noLinks() throws IOException { + Map> links = this.linkExtractor + .extractLinks(createResponse("no-links")); + assertLinks(Collections. emptyList(), links); + } + + @Test + public void linksInTheWrongFormat() throws IOException { + Map> links = this.linkExtractor + .extractLinks(createResponse("wrong-format")); + assertLinks(Collections. emptyList(), links); + } + + private void assertLinks(List expectedLinks, Map> actualLinks) { + Map> expectedLinksByRel = new HashMap<>(); + for (Link expectedLink : expectedLinks) { + List expectedlinksWithRel = expectedLinksByRel.get(expectedLink + .getRel()); + if (expectedlinksWithRel == null) { + expectedlinksWithRel = new ArrayList<>(); + expectedLinksByRel.put(expectedLink.getRel(), expectedlinksWithRel); + } + expectedlinksWithRel.add(expectedLink); + } + assertEquals(expectedLinksByRel, actualLinks); + } + + private MockHttpServletResponse createResponse(String contentName) throws IOException { + MockHttpServletResponse response = new MockHttpServletResponse(); + FileCopyUtils.copy(new FileReader(getPayloadFile(contentName)), + response.getWriter()); + return response; + } + + private File getPayloadFile(String name) { + return new File("src/test/resources/link-payloads/" + linkType + "/" + name + + ".json"); + } +} diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json new file mode 100644 index 00000000..35e63537 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json @@ -0,0 +1,9 @@ +{ + "links": [ { + "rel": "alpha", + "href": "http://alpha.example.com" + }, { + "rel": "bravo", + "href": "http://bravo.example.com" + } ] +} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json new file mode 100644 index 00000000..ff0d3f4a --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json @@ -0,0 +1,9 @@ +{ + "links": [ { + "rel": "alpha", + "href": "http://alpha.example.com/one" + }, { + "rel": "alpha", + "href": "http://alpha.example.com/two" + } ] +} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/no-links.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/no-links.json new file mode 100644 index 00000000..6f31cf5a --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/no-links.json @@ -0,0 +1 @@ +{ } \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json new file mode 100644 index 00000000..57532675 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json @@ -0,0 +1,6 @@ +{ + "links": [ { + "rel": "alpha", + "href": "http://alpha.example.com" + } ] +} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json new file mode 100644 index 00000000..04a6d84b --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json @@ -0,0 +1,5 @@ +{ + "_links": { + "alpha": ["http://alpha.example.com/one", "http://alpha.example.com/two"] + } +} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json new file mode 100644 index 00000000..80d36d72 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json @@ -0,0 +1,6 @@ +{ + "_links": { + "alpha": "http://alpha.example.com", + "bravo": "http://bravo.example.com" + } +} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json new file mode 100644 index 00000000..04a6d84b --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json @@ -0,0 +1,5 @@ +{ + "_links": { + "alpha": ["http://alpha.example.com/one", "http://alpha.example.com/two"] + } +} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/no-links.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/no-links.json new file mode 100644 index 00000000..6f31cf5a --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/no-links.json @@ -0,0 +1 @@ +{ } \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json new file mode 100644 index 00000000..be90b377 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json @@ -0,0 +1,5 @@ +{ + "_links": { + "alpha": "http://alpha.example.com" + } +} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/wrong-format.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/wrong-format.json new file mode 100644 index 00000000..7c99aa4e --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/wrong-format.json @@ -0,0 +1,9 @@ +{ + "_links": [ { + "rel": "alpha", + "href": "http://alpha.example.com/one" + }, { + "rel": "alpha", + "href": "http://alpha.example.com/two" + } ] +} \ No newline at end of file