Skip to content

Commit 68bf83d

Browse files
authored
fix: [OData] Remove duplicate query parameters from nextLink references (#813)
1 parent 7cfd13c commit 68bf83d

File tree

5 files changed

+93
-9
lines changed

5 files changed

+93
-9
lines changed

cloudplatform/connectivity-apache-httpclient4/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/HttpClientWrapper.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
* and it will append the url configured in the destination.
3232
*/
3333
@Slf4j
34-
class HttpClientWrapper extends CloseableHttpClient
34+
class HttpClientWrapper extends CloseableHttpClient implements UriQueryMerger
3535
{
3636
private final CloseableHttpClient httpClient;
3737
@Getter( AccessLevel.PACKAGE )
@@ -107,15 +107,20 @@ HttpClientWrapper withDestination( final HttpDestinationProperties destination )
107107
return new HttpClientWrapper(httpClient, destination);
108108
}
109109

110-
HttpUriRequest wrapRequest( final HttpUriRequest request )
110+
@Nonnull
111+
public URI mergeRequestUri( @Nonnull final URI requestUri )
111112
{
112113
final UriPathMerger merger = new UriPathMerger();
113-
URI requestUri = merger.merge(destination.getUri(), request.getURI());
114+
final URI mergedUri = merger.merge(destination.getUri(), requestUri);
114115

115116
final String queryString = Joiner.on("&").join(QueryParamGetter.getQueryParameters(destination));
116-
requestUri = merger.merge(requestUri, URI.create("/?" + queryString));
117+
return merger.merge(mergedUri, URI.create("/?" + queryString));
118+
}
117119

120+
HttpUriRequest wrapRequest( final HttpUriRequest request )
121+
{
118122
final RequestBuilder requestBuilder = RequestBuilder.copy(request);
123+
final URI requestUri = mergeRequestUri(request.getURI());
119124
requestBuilder.setUri(requestUri);
120125

121126
for( final Header header : destination.getHeaders(requestUri) ) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.sap.cloud.sdk.cloudplatform.connectivity;
2+
3+
import java.net.URI;
4+
5+
import javax.annotation.Nonnull;
6+
7+
/**
8+
* Interface to resolve the request URI for a given request. This is used to determine the full request URI.
9+
*/
10+
@FunctionalInterface
11+
public interface UriQueryMerger
12+
{
13+
/**
14+
* Returns the request URI for the given request. This method is used to resolve the request URI
15+
*
16+
* @param requestUri
17+
* the request uri to merge.
18+
* @return the request URI.
19+
*/
20+
@Nonnull
21+
URI mergeRequestUri( @Nonnull final URI requestUri );
22+
}

datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultGeneric.java

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.google.gson.JsonElement;
3535
import com.google.gson.JsonParser;
3636
import com.google.gson.stream.JsonToken;
37+
import com.sap.cloud.sdk.cloudplatform.connectivity.UriQueryMerger;
3738
import com.sap.cloud.sdk.datamodel.odata.client.JsonPath;
3839
import com.sap.cloud.sdk.datamodel.odata.client.ODataProtocol;
3940
import com.sap.cloud.sdk.datamodel.odata.client.ODataResponseDeserializer;
@@ -554,10 +555,10 @@ public Option<String> getNextLink()
554555
for( final JsonPath path : getODataRequest().getProtocol().getPathToNextLink().getPaths() ) {
555556
final ResultElement resultElement = getResultElement(path);
556557
if( resultElement != null ) {
557-
return Option
558-
.of(resultElement)
559-
.map(ResultElement::asString)
560-
.peek(link -> log.debug("Found reference to next page: {}", link));
558+
String nextLink = resultElement.asString();
559+
log.debug("Found reference to next page: {}", nextLink);
560+
nextLink = removeDuplicateQueryParameters(nextLink);
561+
return Option.of(nextLink);
561562
}
562563
}
563564
log.debug("Result does not reference any further pages.");
@@ -751,4 +752,31 @@ private void assertResultTypeIsNotVoid( @Nonnull final Class<?> cls )
751752
}
752753

753754
}
755+
756+
@Nonnull
757+
private String removeDuplicateQueryParameters( @Nonnull final String nextLink )
758+
{
759+
if( !(httpClient instanceof UriQueryMerger) ) {
760+
return nextLink;
761+
}
762+
final String query = ((UriQueryMerger) httpClient).mergeRequestUri(URI.create("")).getRawQuery();
763+
if( query == null ) {
764+
return nextLink;
765+
}
766+
final String[] segments = nextLink.split("\\?", 2);
767+
final String[] queryArguments = query.split("&");
768+
for( final String argument : queryArguments ) {
769+
if( segments[1].contains(argument) ) {
770+
segments[1] = segments[1].replace(argument, "");
771+
}
772+
}
773+
if( nextLink.length() + 1 == segments[0].length() + segments[1].length() ) {
774+
return nextLink;
775+
}
776+
// after removal of arguments clean-up query: fix "?foo=bar&&&one=1", fix "?&one=1", fix "?foo=bar&"
777+
segments[1] = segments[1].replaceAll("&&+", "&").replace("?&", "?").replaceAll("&$", "");
778+
final String updatedLink = segments[0] + "?" + segments[1];
779+
log.debug("Updated reference to next page: {}", updatedLink);
780+
return updatedLink;
781+
}
754782
}

datamodel/odata-client/src/test/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataNextLinkTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@
66

77
import org.apache.http.HttpResponse;
88
import org.apache.http.HttpVersion;
9+
import org.apache.http.client.HttpClient;
910
import org.apache.http.entity.ContentType;
1011
import org.apache.http.entity.StringEntity;
1112
import org.apache.http.message.BasicHttpResponse;
1213
import org.junit.jupiter.api.Test;
1314

15+
import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination;
16+
import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
17+
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpClientAccessor;
1418
import com.sap.cloud.sdk.datamodel.odata.client.ODataProtocol;
1519

1620
class ODataNextLinkTest
@@ -24,6 +28,30 @@ class ODataNextLinkTest
2428
}
2529
""";
2630

31+
@Test
32+
void testRemoveDuplicateQueryArguments()
33+
{
34+
final ODataRequestGeneric request =
35+
new ODataRequestRead("/v1/foo/bar/", "endpoint", "blub=42", ODataProtocol.V2);
36+
37+
final HttpResponse httpResponse = new BasicHttpResponse(HttpVersion.HTTP_1_1, 200, "Ok");
38+
httpResponse.setEntity(new StringEntity(PAYLOAD_NEXT_LINK, ContentType.APPLICATION_JSON));
39+
40+
final String baseUrl = "http://blub/?high=five";
41+
42+
// case 1: query parameters are EQUAL in destination and in nextLink -> remove redundant query parameter
43+
final Destination dest1 = DefaultHttpDestination.builder(baseUrl).property("URL.queries.foo", "bar").build();
44+
final HttpClient client1 = HttpClientAccessor.getHttpClient(dest1);
45+
final ODataRequestResultGeneric result1 = new ODataRequestResultGeneric(request, httpResponse, client1);
46+
assertThat(result1.getNextLink()).contains("/v1/foo/bar/endpoint?$skiptoken=s3cReT-t0k3n");
47+
48+
// case 2: query parameters are NOT EQUAL in destination and in nextLink -> retain query parameter
49+
final Destination dest2 = DefaultHttpDestination.builder(baseUrl).property("URL.queries.foo", "baz").build();
50+
final HttpClient client2 = HttpClientAccessor.getHttpClient(dest2);
51+
final ODataRequestResultGeneric result2 = new ODataRequestResultGeneric(request, httpResponse, client2);
52+
assertThat(result2.getNextLink()).contains("/v1/foo/bar/endpoint?$skiptoken=s3cReT-t0k3n&foo=bar");
53+
}
54+
2755
@Test
2856
void testNotParsedNextLinkV4()
2957
{

release_notes.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@
2020

2121
### 🐛 Fixed Issues
2222

23-
- Fixed OData V2 error: Disable Validation in the absense of DOCTYPE
23+
- Fix OData V2 error: Disable Validation in the absense of DOCTYPE
24+
- Fix OData V2/V4 error: Navigating `nextLink` in paginated result-set no longer results in duplicate query parameters.

0 commit comments

Comments
 (0)