diff --git a/src/main/java/org/springframework/hateoas/client/Hop.java b/src/main/java/org/springframework/hateoas/client/Hop.java index 44862423..3d887f61 100644 --- a/src/main/java/org/springframework/hateoas/client/Hop.java +++ b/src/main/java/org/springframework/hateoas/client/Hop.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2018 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. @@ -33,6 +33,7 @@ import org.springframework.util.Assert; * * @author Greg Turnquist * @author Oliver Gierke + * @author Manish Misra * @since 0.18 */ @Value @@ -50,6 +51,9 @@ public class Hop { */ private final @Wither Map parameters; + /** + * Extra {@link HttpHeaders} to apply to this hop. + */ private final @Wither HttpHeaders headers; /** @@ -62,7 +66,7 @@ public class Hop { Assert.hasText(rel, "Relation must not be null or empty!"); - return new Hop(rel, Collections. emptyMap(), null); + return new Hop(rel, Collections.emptyMap(), HttpHeaders.EMPTY); } /** @@ -76,13 +80,10 @@ public class Hop { Assert.hasText(name, "Name must not be null or empty!"); - HashMap parameters = new HashMap(this.parameters); + HashMap parameters = new HashMap<>(this.parameters); parameters.put(name, value); - if (this.hasHeaders()) - return new Hop(rel, parameters, this.headers); - else - return new Hop(rel, parameters, null); + return new Hop(this.rel, parameters, this.headers); } /** @@ -92,20 +93,21 @@ public class Hop { * @param headerValue can be {@literal null}. * @return */ - public Hop withHeader(String headerName, String headerValue){ - Assert.notNull(headers, "Headers must not be null!"); + public Hop header(String headerName, String headerValue) { - HttpHeaders headers = new HttpHeaders(); + Assert.notNull(headerName, "headerName must not be null!"); + Assert.notNull(headerValue, "headerValue must not be null!"); - if(this.hasHeaders()) - headers.addAll(this.headers); + if (this.headers == HttpHeaders.EMPTY) { - headers.add(headerName, headerValue); + HttpHeaders newHeaders = new HttpHeaders(); + newHeaders.add(headerName, headerValue); - if(this.hasParameters()) - return new Hop(rel, this.parameters, headers); - else - return new Hop(rel, Collections. emptyMap(), headers); + return new Hop(this.rel, this.parameters, newHeaders); + } + + this.headers.add(headerName, headerValue); + return this; } /** @@ -117,15 +119,6 @@ public class Hop { return !this.parameters.isEmpty(); } - /** - * Returns whether the {@link Hop} has headers declared. - * - * @return - */ - boolean hasHeaders() { - return this.headers != null && !this.headers.isEmpty(); - } - /** * Create a new {@link Map} starting with the supplied template parameters. Then add the ones for this hop. This * allows a local hop to override global parameters. @@ -144,20 +137,4 @@ public class Hop { return mergedParameters; } - - /** - * Create a new {@link Map} starting with the supplied headers. Then add the ones for this hop. This - * allows a local hop to override global headers. - * - * @param globalHeaders must not be {@literal null}. - * @return - */ - HttpHeaders getMergedHeaders(HttpHeaders globalHeaders){ - Assert.notNull(globalHeaders, "Global Headers must not be null!"); - - if(this.hasHeaders()) - globalHeaders.addAll(this.headers); - - return globalHeaders; - } } diff --git a/src/main/java/org/springframework/hateoas/client/Traverson.java b/src/main/java/org/springframework/hateoas/client/Traverson.java index 5c90905a..e42db23f 100644 --- a/src/main/java/org/springframework/hateoas/client/Traverson.java +++ b/src/main/java/org/springframework/hateoas/client/Traverson.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2017 the original author or authors. + * Copyright 2013-2018 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. @@ -17,6 +17,9 @@ package org.springframework.hateoas.client; import static org.springframework.http.HttpMethod.*; +import lombok.RequiredArgsConstructor; +import lombok.Value; + import java.net.URI; import java.nio.charset.Charset; import java.util.ArrayList; @@ -28,7 +31,6 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import com.sun.jndi.toolkit.url.Uri; import org.springframework.core.ParameterizedTypeReference; import org.springframework.hateoas.Link; import org.springframework.hateoas.LinkDiscoverer; @@ -63,6 +65,7 @@ import com.jayway.jsonpath.JsonPath; * @author Dietrich Schulten * @author Greg Turnquist * @author Tom Bunting + * @author Manish Misra * @since 0.11 */ public class Traverson { @@ -191,7 +194,7 @@ public class Traverson { */ public Traverson setRestOperations(RestOperations operations) { - this.operations = operations == null ? createDefaultTemplate(mediaTypes) : operations; + this.operations = operations == null ? createDefaultTemplate(this.mediaTypes) : operations; return this; } @@ -204,7 +207,7 @@ public class Traverson { */ public Traverson setLinkDiscoverers(List discoverer) { - this.discoverers = discoverers == null ? DEFAULT_LINK_DISCOVERERS + this.discoverers = this.discoverers == null ? DEFAULT_LINK_DISCOVERERS : new LinkDiscoverers(OrderAwarePluginRegistry.create(discoverer)); return this; @@ -300,6 +303,8 @@ public class Traverson { */ public TraversalBuilder withTemplateParameters(Map parameters) { + Assert.notNull(parameters, "Parameters must not be null!"); + this.templateParameters = parameters; return this; } @@ -312,6 +317,8 @@ public class Traverson { */ public TraversalBuilder withHeaders(HttpHeaders headers) { + Assert.notNull(headers, "Headers must not be null!"); + this.headers = headers; return this; } @@ -326,8 +333,10 @@ public class Traverson { Assert.notNull(type, "Target type must not be null!"); - LinkWithHeader linkWithHeader = traverseToExpandedFinalUrl(); - return operations.exchange(linkWithHeader.uriAsURI, GET, prepareRequest(linkWithHeader.httpHeaders), type).getBody(); + URIAndHeaders uriAndHeaders = traverseToExpandedFinalUrl(); + HttpEntity requestEntity = prepareRequest(mergeHeaders(this.headers, uriAndHeaders.getHttpHeaders())); + + return operations.exchange(uriAndHeaders.getUri(), GET, requestEntity, type).getBody(); } /** @@ -341,8 +350,10 @@ public class Traverson { Assert.notNull(type, "Target type must not be null!"); - LinkWithHeader linkWithHeader = traverseToExpandedFinalUrl(); - return operations.exchange(linkWithHeader.uriAsURI, GET, prepareRequest(linkWithHeader.httpHeaders), type).getBody(); + URIAndHeaders uriAndHeaders = traverseToExpandedFinalUrl(); + HttpEntity requestEntity = prepareRequest(mergeHeaders(this.headers, uriAndHeaders.getHttpHeaders())); + + return operations.exchange(uriAndHeaders.getUri(), GET, requestEntity, type).getBody(); } /** @@ -356,9 +367,10 @@ public class Traverson { Assert.hasText(jsonPath, "JSON path must not be null or empty!"); - LinkWithHeader linkWithHeader = traverseToExpandedFinalUrl(); - String forObject = operations.exchange(linkWithHeader.uriAsURI, GET, prepareRequest(linkWithHeader.httpHeaders), String.class) - .getBody(); + URIAndHeaders uriAndHeaders = traverseToExpandedFinalUrl(); + HttpEntity requestEntity = prepareRequest(mergeHeaders(this.headers, uriAndHeaders.getHttpHeaders())); + + String forObject = operations.exchange(uriAndHeaders.getUri(), GET, requestEntity, String.class).getBody(); return JsonPath.read(forObject, jsonPath); } @@ -371,8 +383,11 @@ public class Traverson { public ResponseEntity toEntity(Class type) { Assert.notNull(type, "Target type must not be null!"); - LinkWithHeader linkWithHeader = traverseToExpandedFinalUrl(); - return operations.exchange(linkWithHeader.uriAsURI, GET, prepareRequest(linkWithHeader.httpHeaders), type); + + URIAndHeaders uriAndHeaders = traverseToExpandedFinalUrl(); + HttpEntity requestEntity = prepareRequest(mergeHeaders(this.headers, uriAndHeaders.getHttpHeaders())); + + return operations.exchange(uriAndHeaders.getUri(), GET, requestEntity, type); } /** @@ -399,28 +414,34 @@ public class Traverson { private Link traverseToLink(boolean expandFinalUrl) { - Assert.isTrue(rels.size() > 0, "At least one rel needs to be provided!"); - return new Link(expandFinalUrl ? traverseToExpandedFinalUrl().uriAsURI.toString() : traverseToFinalUrl(), - rels.get(rels.size() - 1).getRel()); + Assert.isTrue(this.rels.size() > 0, "At least one rel needs to be provided!"); + + URIAndHeaders expandedFinalUriAndHeaders = traverseToExpandedFinalUrl(); + UriStringAndHeaders finalUriAndHeaders = traverseToFinalUrl(); + + return new Link(expandFinalUrl ? expandedFinalUriAndHeaders.getUri().toString() : finalUriAndHeaders.getUri(), + this.rels.get(this.rels.size() - 1).getRel()); } - private String traverseToFinalUrl() { + private UriStringAndHeaders traverseToFinalUrl() { - return getAndFindLinkWithRel(baseUri.toString(), rels.iterator(), headers).uriAsString; + UriStringAndHeaders uriAndHeaders = getAndFindLinkWithRel(baseUri.toString(), this.rels.iterator(), HttpHeaders.EMPTY); + return new UriStringAndHeaders(new UriTemplate(uriAndHeaders.getUri()).toString(), uriAndHeaders.getHttpHeaders()); } - private LinkWithHeader traverseToExpandedFinalUrl() { + private URIAndHeaders traverseToExpandedFinalUrl() { - return getAndFindLinkWithRel(baseUri.toString(), rels.iterator(), headers); + UriStringAndHeaders uriAndHeaders = getAndFindLinkWithRel(baseUri.toString(), this.rels.iterator(), HttpHeaders.EMPTY); + return new URIAndHeaders(new UriTemplate(uriAndHeaders.getUri()).expand(this.templateParameters), uriAndHeaders.getHttpHeaders()); } - private LinkWithHeader getAndFindLinkWithRel(String uri, Iterator rels, HttpHeaders headers) { + private UriStringAndHeaders getAndFindLinkWithRel(String uri, Iterator rels, HttpHeaders extraHeaders) { if (!rels.hasNext()) { - return new LinkWithHeader(uri, headers); + return new UriStringAndHeaders(uri, extraHeaders); } - HttpEntity request = prepareRequest(headers); + HttpEntity request = prepareRequest(mergeHeaders(this.headers, extraHeaders)); UriTemplate template = new UriTemplate(uri); ResponseEntity responseEntity = operations.exchange(template.expand(), GET, request, String.class); @@ -437,50 +458,53 @@ public class Traverson { String.format("Expected to find link with rel '%s' in response %s!", rel, responseBody)); } - /** + /* * Don't expand if the parameters are empty */ if (!thisHop.hasParameters()) { - return getAndFindLinkWithRel(link.getHref(), rels, thisHop.getMergedHeaders(this.headers)); + return getAndFindLinkWithRel(link.getHref(), rels, thisHop.getHeaders()); } else { - return getAndFindLinkWithRel(link.expand(thisHop.getMergedParameters(templateParameters)).getHref(), rels, thisHop.getMergedHeaders(this.headers)); + return getAndFindLinkWithRel(link.expand(thisHop.getMergedParameters(this.templateParameters)).getHref(), rels, thisHop.getHeaders()); } } - private class LinkWithHeader{ - private String uriAsString; - private URI uriAsURI; - private HttpHeaders httpHeaders; + /** + * Combine two sets of {@link HttpHeaders} into one. + * + * @param headersA + * @param headersB + * @return + */ + private HttpHeaders mergeHeaders(HttpHeaders headersA, HttpHeaders headersB) { - LinkWithHeader(String uri, HttpHeaders httpHeaders){ - this.uriAsString = new UriTemplate(uri).toString(); - this.uriAsURI = new UriTemplate(uri).expand(templateParameters); - this.httpHeaders = httpHeaders; - } + HttpHeaders mergedHeaders = new HttpHeaders(); - public URI getUriAsURI() { - return uriAsURI; - } + mergedHeaders.addAll(headersA); + mergedHeaders.addAll(headersB); - public void setUriAsURI(URI uriAsURI) { - this.uriAsURI = uriAsURI; - } - - public String getUriAsString() { - return uriAsString; - } - - public void setUriAsString(String uriAsString) { - this.uriAsString = uriAsString; - } - - public HttpHeaders getHttpHeaders() { - return httpHeaders; - } - - public void setHttpHeaders(HttpHeaders httpHeaders) { - this.httpHeaders = httpHeaders; - } + return mergedHeaders; } } + + /** + * Temporary container for a string-base {@literal URI} and {@link HttpHeaders}. + */ + @Value + @RequiredArgsConstructor + private static class UriStringAndHeaders { + + private final String uri; + private final HttpHeaders httpHeaders; + } + + /** + * Temporary container for a {@link URI}-based {@literal URI} and {@link HttpHeaders}. + */ + @Value + @RequiredArgsConstructor + private static class URIAndHeaders { + + private final URI uri; + private final HttpHeaders httpHeaders; + } } diff --git a/src/test/java/org/springframework/hateoas/client/TraversonTest.java b/src/test/java/org/springframework/hateoas/client/TraversonTest.java index 9d272687..0004e3df 100755 --- a/src/test/java/org/springframework/hateoas/client/TraversonTest.java +++ b/src/test/java/org/springframework/hateoas/client/TraversonTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2017 the original author or authors. + * Copyright 2013-2018 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. @@ -58,23 +58,26 @@ import org.springframework.web.client.RestTemplate; public class TraversonTest { URI baseUri; + Server server; + Traverson traverson; @Before public void setUp() { this.server = new Server(); - this.baseUri = URI.create(server.rootResource()); - this.traverson = new Traverson(baseUri, MediaTypes.HAL_JSON_UTF8, MediaTypes.HAL_JSON); + this.baseUri = URI.create(this.server.rootResource()); + this.traverson = new Traverson(this.baseUri, MediaTypes.HAL_JSON_UTF8, MediaTypes.HAL_JSON); setUpActors(); } @After public void tearDown() throws IOException { - if (server != null) { - server.close(); + + if (this.server != null) { + this.server.close(); } } @@ -91,7 +94,7 @@ public class TraversonTest { */ @Test(expected = IllegalArgumentException.class) public void rejectsEmptyMediaTypes() { - new Traverson(baseUri); + new Traverson(this.baseUri); } /** @@ -121,6 +124,7 @@ public class TraversonTest { */ @Test public void readsJsonPathTraversalIntoJsonPathExpression() { + assertThat(traverson.follow(// "$._links.movies.href", // "$._links.movie.href", // @@ -189,7 +193,7 @@ public class TraversonTest { RestTemplate restTemplate = new RestTemplate(); restTemplate.setInterceptors(Arrays.asList(interceptor)); - this.traverson = new Traverson(baseUri, MediaTypes.HAL_JSON); + this.traverson = new Traverson(this.baseUri, MediaTypes.HAL_JSON); this.traverson.setRestOperations(restTemplate); traverson.follow("movies", "movie", "actor"). toObject("$.name"); @@ -202,7 +206,7 @@ public class TraversonTest { @Test public void usesCustomLinkDiscoverer() { - this.traverson = new Traverson(URI.create(server.rootResource() + "/github"), MediaType.APPLICATION_JSON); + this.traverson = new Traverson(URI.create(this.server.rootResource() + "/github"), MediaType.APPLICATION_JSON); this.traverson.setLinkDiscoverers(Arrays.asList(new GitHubLinkDiscoverer())); String value = this.traverson.follow("foo").toObject("$.key"); @@ -227,7 +231,7 @@ public class TraversonTest { @Test public void returnsTemplatedLinkIfRequested() { - TraversalBuilder follow = new Traverson(URI.create(server.rootResource().concat("/link")), MediaTypes.HAL_JSON) + TraversalBuilder follow = new Traverson(URI.create(this.server.rootResource().concat("/link")), MediaTypes.HAL_JSON) .follow("self"); Link link = follow.asTemplatedLink(); @@ -306,7 +310,7 @@ public class TraversonTest { .isEqualTo(server.rootResource() + "/springagram/items/1"); final Item item = itemResource.getContent(); - assertThat(item.image).isEqualTo(server.rootResource() + "/springagram/file/cat"); + assertThat(item.image).isEqualTo(this.server.rootResource() + "/springagram/file/cat"); assertThat(item.description).isEqualTo("cat"); } @@ -316,7 +320,7 @@ public class TraversonTest { @Test public void allowAlteringTheDetailsOfASingleHopByMapOperations() { - this.traverson = new Traverson(URI.create(server.rootResource() + "/springagram"), MediaTypes.HAL_JSON); + this.traverson = new Traverson(URI.create(this.server.rootResource() + "/springagram"), MediaTypes.HAL_JSON); // tag::hop-put[] ParameterizedTypeReference> resourceParameterizedTypeReference = new ParameterizedTypeReference>() {}; @@ -331,10 +335,10 @@ public class TraversonTest { assertThat(itemResource.hasLink("self")).isTrue(); assertThat(itemResource.getRequiredLink("self").expand().getHref()) - .isEqualTo(server.rootResource() + "/springagram/items/1"); + .isEqualTo(this.server.rootResource() + "/springagram/items/1"); final Item item = itemResource.getContent(); - assertThat(item.image).isEqualTo(server.rootResource() + "/springagram/file/cat"); + assertThat(item.image).isEqualTo(this.server.rootResource() + "/springagram/file/cat"); assertThat(item.description).isEqualTo("cat"); } @@ -344,7 +348,7 @@ public class TraversonTest { @Test public void allowGlobalsToImpactSingleHops() { - this.traverson = new Traverson(URI.create(server.rootResource() + "/springagram"), MediaTypes.HAL_JSON); + this.traverson = new Traverson(URI.create(this.server.rootResource() + "/springagram"), MediaTypes.HAL_JSON); Map params = new HashMap<>(); params.put("projection", "thisShouldGetOverwrittenByLocalHop"); @@ -356,10 +360,10 @@ public class TraversonTest { assertThat(itemResource.hasLink("self")).isTrue(); assertThat(itemResource.getRequiredLink("self").expand().getHref()) - .isEqualTo(server.rootResource() + "/springagram/items/1"); + .isEqualTo(this.server.rootResource() + "/springagram/items/1"); final Item item = itemResource.getContent(); - assertThat(item.image).isEqualTo(server.rootResource() + "/springagram/file/cat"); + assertThat(item.image).isEqualTo(this.server.rootResource() + "/springagram/file/cat"); assertThat(item.description).isEqualTo("cat"); } @@ -369,7 +373,7 @@ public class TraversonTest { @Test public void doesNotDoubleEncodeURI() { - this.traverson = new Traverson(URI.create(server.rootResource() + "/springagram"), MediaTypes.HAL_JSON); + this.traverson = new Traverson(URI.create(this.server.rootResource() + "/springagram"), MediaTypes.HAL_JSON); Resource itemResource = traverson.// follow(rel("items").withParameters(Collections.singletonMap("projection", "no images"))).// @@ -377,20 +381,59 @@ public class TraversonTest { assertThat(itemResource.hasLink("self")).isTrue(); assertThat(itemResource.getRequiredLink("self").expand().getHref()) - .isEqualTo(server.rootResource() + "/springagram/items"); + .isEqualTo(this.server.rootResource() + "/springagram/items"); } + @Test + public void customHeaders() { + + String customHeaderName = "X-CustomHeader"; + + traverson + .follow(rel("movies") + .header(customHeaderName, "alpha") + .header(HttpHeaders.LOCATION, "http://localhost:8080/my/custom/location")) + .follow(rel("movie").header(customHeaderName, "bravo")) + .follow(rel("actor").header(customHeaderName, "charlie")) + .toObject("$.name"); + + verifyThatRequest() // + .havingPathEqualTo("/") // + .havingHeader(HttpHeaders.ACCEPT, contains(MediaTypes.HAL_JSON_UTF8_VALUE + ", " + MediaTypes.HAL_JSON_VALUE)) // + .receivedOnce(); + + verifyThatRequest() + .havingPathEqualTo("/movies") // aggregate root movies + .havingHeader(HttpHeaders.ACCEPT, contains(MediaTypes.HAL_JSON_UTF8_VALUE + ", " + MediaTypes.HAL_JSON_VALUE)) // + .havingHeader(customHeaderName, contains("alpha")) // + .havingHeader(HttpHeaders.LOCATION, contains("http://localhost:8080/my/custom/location")) // + .receivedOnce(); + + verifyThatRequest() + .havingPath(startsWith("/movies/")) // single movie + .havingHeader(HttpHeaders.ACCEPT, contains(MediaTypes.HAL_JSON_UTF8_VALUE + ", " + MediaTypes.HAL_JSON_VALUE)) // + .havingHeader(customHeaderName, contains("bravo")) // + .receivedOnce(); + + verifyThatRequest() + .havingPath(startsWith("/actors/")) // single actor + .havingHeader(HttpHeaders.ACCEPT, contains(MediaTypes.HAL_JSON_UTF8_VALUE + ", " + MediaTypes.HAL_JSON_VALUE)) // + .havingHeader(customHeaderName, contains("charlie")) // + .receivedOnce(); + } + + private void setUpActors() { Resource actor = new Resource<>(new Actor("Keanu Reaves")); - String actorUri = server.mockResourceFor(actor); + String actorUri = this.server.mockResourceFor(actor); Movie movie = new Movie("The Matrix"); Resource resource = new Resource<>(movie); resource.add(new Link(actorUri, "actor")); - server.mockResourceFor(resource); - server.finishMocking(); + this.server.mockResourceFor(resource); + this.server.finishMocking(); } static class CountingInterceptor implements ClientHttpRequestInterceptor {