Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Pagination #806

Merged
merged 5 commits into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 6 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ For more examples of handling collections see the [paging](#paging) section belo
UserApi userApi = new UserApi(client);

// search by email
List<User> users = userApi.listUsers(null, null, 5, null, "[email protected]", null, null);
List<User> users = userApi.listUsers(null, null, 5, null, "profile.email eq \"[email protected]\"", null, null);

// filter parameter
users = userApi.listUsers(null, null, null, "status eq \"ACTIVE\"",null, null, null);
Expand Down Expand Up @@ -437,29 +437,19 @@ Every instance of the SDK `Client` is thread-safe. You **should** use the same i

## Paging

Paging is handled automatically when iterating over a collection.

[//]: # (method: paging)
```java
UserApi userApi = new UserApi(client);
int limit = 2;
PagedList<User> pagedUserList = userApi.listUsersWithPaginationInfo(null, null, limit, null, null, null, null);

// limit
int pageSize = 2;
PagedList<User> usersPagedListOne = userApi.listUsersWithPaginationInfo(null, null, pageSize, null, null, null, null);

// e.g. https://example.okta.com/api/v1/users?after=000u3pfv9v4SQXvpBB0g7&limit=2
String nextPageUrl = usersPagedListOne.getNextPage();

// replace 'after' with actual cursor from the nextPageUrl
PagedList<User> usersPagedListTwo = userApi.listUsersWithPaginationInfo("after", null, pageSize, null, null, null, null);

// loop through all of them (paging is automatic)
for (User tmpUser : usersPagedListOne.getItems()) {
// loop through all of them
for (User tmpUser : pagedUserList.getItems()) {
log.info("User: {}", tmpUser.getProfile().getEmail());
}

// or stream
usersPagedListOne.getItems().forEach(tmpUser -> log.info("User: {}", tmpUser.getProfile().getEmail()));
pagedUserList.getItems().forEach(tmpUser -> log.info("User: {}", tmpUser.getProfile().getEmail()));
```
[//]: # (end: paging)

Expand Down
2 changes: 1 addition & 1 deletion api/src/main/java/com/okta/sdk/error/ErrorHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public void handleError(ClientHttpResponse httpResponse) throws IOException, Res
final String message = new String(FileCopyUtils.copyToByteArray(httpResponse.getBody()));

if (!isValid(message)) {
throw new ResourceException(new NonJsonError(message));
throw new ResourceException(new NonJsonError(statusCode, message));
}

final Map<String, Object> errorMap = mapper.readValue(message, Map.class);
Expand Down
6 changes: 4 additions & 2 deletions api/src/main/java/com/okta/sdk/error/NonJsonError.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@

public class NonJsonError implements Error {

private final int status;
private final String message;

public NonJsonError(String message) {
public NonJsonError(int status, String message) {
this.status = status;
this.message = message;
}

@Override
public int getStatus() {
return 0;
return status;
}

@Override
Expand Down
58 changes: 58 additions & 0 deletions api/src/main/java/com/okta/sdk/resource/common/PagedList.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,17 @@
*/
package com.okta.sdk.resource.common;

import com.okta.commons.lang.Assert;
import org.springframework.http.ResponseEntity;

import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand All @@ -37,6 +47,16 @@ public String getNextPage() {
return nextPage;
}

public String getAfter(String nextPageUrl) {
URL url;
try {
url = new URL(nextPageUrl);
return splitQuery(url).get("after");
} catch (MalformedURLException | UnsupportedEncodingException e) {
return null;
}
}

public void setSelf(String self) {
this.self = self;
}
Expand All @@ -58,4 +78,42 @@ private List<?> flatten(List<?> list) {
.flatMap(e -> e instanceof List ? flatten((List) e).stream() : Stream.of(e))
.collect(Collectors.toList());
}

public static PagedList constructPagedList(ResponseEntity responseEntity) {

PagedList pagedList = new PagedList();
Assert.notNull(responseEntity);
pagedList.addItems(Collections.singletonList(responseEntity.getBody()));
List<String> linkHeaders = responseEntity.getHeaders().get("link");
Assert.notNull(linkHeaders);
for (String link : linkHeaders) {
String[] parts = link.split("; *");
String url = parts[0]
.replaceAll("<", "")
.replaceAll(">", "");
String rel = parts[1];
if (rel.equals("rel=\"next\"")) {
pagedList.setNextPage(url);
} else if (rel.equals("rel=\"self\"")) {
pagedList.setSelf(url);
}
}
return pagedList;
}

/**
* Split a URL with query strings into name value pairs.
* @param url the url to split
* @return map of query string name value pairs
*/
private static Map<String, String> splitQuery(URL url) throws UnsupportedEncodingException {
Map<String, String> query_pairs = new LinkedHashMap<>();
String query = url.getQuery();
String[] pairs = query.split("&");
for (String pair : pairs) {
int index = pair.indexOf("=");
query_pairs.put(URLDecoder.decode(pair.substring(0, index), "UTF-8"), URLDecoder.decode(pair.substring(index + 1), "UTF-8"));
}
return query_pairs;
}
}
99 changes: 54 additions & 45 deletions api/src/main/resources/custom_templates/ApiClient.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,17 @@ import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.client.config.RequestConfig;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.StreamUtils;
import java.nio.charset.Charset;
{{#withXml}}
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
Expand Down Expand Up @@ -896,7 +903,7 @@ List<ClientHttpRequestInterceptor> currentInterceptors = this.restTemplate.getIn
RestTemplate restTemplate = new RestTemplate(messageConverters);
{{/withXml}}{{^withXml}}RestTemplate restTemplate = new RestTemplate();{{/withXml}}
// This allows us to read the response more than once - Necessary for debugging.
restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(restTemplate.getRequestFactory()));
restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(getClientHttpRequestFactory()));

// disable default URL encoding
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory();
Expand All @@ -905,29 +912,45 @@ List<ClientHttpRequestInterceptor> currentInterceptors = this.restTemplate.getIn
return restTemplate;
}

private ClientHttpRequestFactory getClientHttpRequestFactory() {
int timeout = 5000; //TODO: set from config

RequestConfig config = RequestConfig.custom()
.setConnectTimeout(timeout)
.setConnectionRequestTimeout(timeout)
.setSocketTimeout(timeout)
.build();

CloseableHttpClient client = HttpClientBuilder
.create()
.setDefaultRequestConfig(config)
.build();

return new HttpComponentsClientHttpRequestFactory(client);
}

protected RetryTemplate buildRetryTemplate() {
Map<Class<? extends Throwable>, Boolean> retryableExceptions = new HashMap<>();
retryableExceptions.put(RetryableException.class, Boolean.TRUE);
Map<Class<? extends Throwable>, Boolean> retryableExceptions = new HashMap<>();
retryableExceptions.put(RetryableException.class, Boolean.TRUE);

SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(5, retryableExceptions); //TODO - get this from client config
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(5, retryableExceptions); //TODO - get this from client config
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();

RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(retryPolicy);
retryTemplate.setBackOffPolicy(backOffPolicy);
retryTemplate.setThrowLastExceptionOnExhausted(true);
return retryTemplate;
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(retryPolicy);
retryTemplate.setBackOffPolicy(backOffPolicy);
retryTemplate.setThrowLastExceptionOnExhausted(true);
return retryTemplate;
}

private ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.registerModule(new JavaTimeModule());
objectMapper.registerModule(new JsonNullableModule());

return objectMapper;
}
private ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.registerModule(new JavaTimeModule());
objectMapper.registerModule(new JsonNullableModule());
return objectMapper;
}

/**
* Update query and header parameters based on authentication settings.
Expand All @@ -947,32 +970,30 @@ List<ClientHttpRequestInterceptor> currentInterceptors = this.restTemplate.getIn
}

private class ApiClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
private final Log log = LogFactory.getLog(ApiClientHttpRequestInterceptor.class);
private final Log logger = LogFactory.getLog(ApiClientHttpRequestInterceptor.class);

@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
logRequest(request, body);
ClientHttpResponse response = execution.execute(request, body);
logResponse(response);
return response;
logRequest(request, body);
ClientHttpResponse response = execution.execute(request, body);
logResponse(response);
return response;
}

private void logRequest(HttpRequest request, byte[] body) throws UnsupportedEncodingException {
log.info("URI: " + request.getURI());
log.info("HTTP Method: " + request.getMethod());
log.info("HTTP Headers: " + headersToString(request.getHeaders()));
log.info("Request Body: " + new String(body, StandardCharsets.UTF_8));
logger.info("URI: " + request.getMethod() + " " + request.getURI());
logger.info("Request Headers: " + headersToString(request.getHeaders()));
logger.info("Request Body: " + new String(body, StandardCharsets.UTF_8));
}

private void logResponse(ClientHttpResponse response) throws IOException {
log.info("HTTP Status Code: " + response.getRawStatusCode());
log.info("Status Text: " + response.getStatusText());
log.info("HTTP Headers: " + headersToString(response.getHeaders()));
log.info("Response Body: " + bodyToString(response.getBody()));
logger.info("Response Status: " + response.getRawStatusCode() + " " + response.getStatusText());
logger.info("Response Headers: " + headersToString(response.getHeaders()));
logger.info("Response Body: " + StreamUtils.copyToString(response.getBody(), Charset.defaultCharset()));
}

private String headersToString(HttpHeaders headers) {
if(headers == null || headers.isEmpty()) {
if (headers == null || headers.isEmpty()) {
return "";
}
StringBuilder builder = new StringBuilder();
Expand All @@ -991,17 +1012,5 @@ List<ClientHttpRequestInterceptor> currentInterceptors = this.restTemplate.getIn
builder.setLength(builder.length() - 1); // Get rid of trailing comma
return builder.toString();
}

private String bodyToString(InputStream body) throws IOException {
StringBuilder builder = new StringBuilder();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(body, StandardCharsets.UTF_8));
String line = bufferedReader.readLine();
while (line != null) {
builder.append(line).append(System.lineSeparator());
line = bufferedReader.readLine();
}
bufferedReader.close();
return builder.toString();
}
}
}
Loading