-
Notifications
You must be signed in to change notification settings - Fork 56
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
#88 | Initial idea to support pagination #90
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,8 +40,7 @@ | |
|
||
import java.io.IOException; | ||
import java.time.Instant; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.*; | ||
import java.util.function.Function; | ||
|
||
import static com.github.castorm.kafka.connect.common.VersionUtils.getVersion; | ||
|
@@ -67,6 +66,16 @@ public class HttpSourceTask extends SourceTask { | |
|
||
private SourceRecordFilterFactory recordFilterFactory; | ||
|
||
private Boolean handlePagination; | ||
|
||
private Boolean appendNextPageUrl; | ||
|
||
private String baseUrl; | ||
|
||
private String modifiedUrl; | ||
|
||
private HttpRequest request = null; | ||
|
||
private ConfirmationWindow<Map<String, ?>> confirmationWindow = new ConfirmationWindow<>(emptyList()); | ||
|
||
@Getter | ||
|
@@ -92,6 +101,10 @@ public void start(Map<String, String> settings) { | |
recordSorter = config.getRecordSorter(); | ||
recordFilterFactory = config.getRecordFilterFactory(); | ||
offset = loadOffset(config.getInitialOffset()); | ||
handlePagination = !Objects.isNull(config.getHandlePagination()) && config.getHandlePagination(); | ||
appendNextPageUrl = !Objects.isNull(config.getAppendNextPageUrl()) && config.getAppendNextPageUrl(); | ||
baseUrl = config.getBaseUrl(); | ||
modifiedUrl = null; | ||
} | ||
|
||
private Offset loadOffset(Map<String, String> initialOffset) { | ||
|
@@ -104,11 +117,37 @@ public List<SourceRecord> poll() throws InterruptedException { | |
|
||
throttler.throttle(offset.getTimestamp().orElseGet(Instant::now)); | ||
|
||
HttpRequest request = requestFactory.createRequest(offset); | ||
// HttpRequest request = requestFactory.createRequest(offset); | ||
|
||
List<SourceRecord> records = new ArrayList<>(); | ||
|
||
if(handlePagination && !Objects.isNull(modifiedUrl)) { | ||
request = HttpRequest.builder() | ||
.method(request.getMethod()) | ||
.url(modifiedUrl) | ||
.headers(request.getHeaders()) | ||
.body(request.getBody()) | ||
.build(); | ||
Comment on lines
+125
to
+130
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are creating a request here. Why not doing it in the entity meant for that: You might want to create a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried to create a different request factory to take care of pagination. But I am stuck at a point. I am not able to understand how to pass the next page URL from the response to this request factory to create a new request. |
||
} else { | ||
request = requestFactory.createRequest(offset); | ||
} | ||
|
||
HttpResponse response = execute(request); | ||
|
||
List<SourceRecord> records = responseParser.parse(response); | ||
records.addAll(responseParser.parse(response)); | ||
|
||
if(handlePagination) { | ||
Optional<String> nextPageUrl = responseParser.getNextPageUrl(response); | ||
log.info("Next page URL: {}", nextPageUrl.orElse("no value")); | ||
if( isNextPageUrlPresent(nextPageUrl) ) { | ||
modifiedUrl = appendNextPageUrl | ||
? baseUrl + nextPageUrl.orElse("") | ||
: nextPageUrl.orElse(null); | ||
} else { | ||
modifiedUrl = null; | ||
} | ||
} | ||
Comment on lines
+139
to
+149
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be extracted somewhere else, it doesn't seem to be at the same level of abstraction as the rest of the code in this method. Too detailed and specific about pagination. |
||
|
||
|
||
List<SourceRecord> unseenRecords = recordSorter.sort(records).stream() | ||
.filter(recordFilterFactory.create(offset)) | ||
|
@@ -129,6 +168,13 @@ private HttpResponse execute(HttpRequest request) { | |
} | ||
} | ||
|
||
private Boolean isNextPageUrlPresent(Optional<String> nextPageUrl) { | ||
return nextPageUrl.isPresent() && | ||
!Objects.isNull(nextPageUrl.orElse(null)) && | ||
!nextPageUrl.orElse(null).equalsIgnoreCase("null"); | ||
} | ||
|
||
|
||
private static List<Map<String, ?>> extractOffsets(List<SourceRecord> recordsToSend) { | ||
return recordsToSend.stream() | ||
.map(SourceRecord::sourceOffset) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,7 @@ | |
|
||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.function.Function; | ||
|
||
import static java.util.Collections.emptyList; | ||
|
@@ -65,4 +66,17 @@ public List<SourceRecord> parse(HttpResponse response) { | |
throw new IllegalStateException(String.format("Policy failed for response code: %s, body: %s", response.getCode(), ofNullable(response.getBody()).map(String::new).orElse(""))); | ||
} | ||
} | ||
|
||
@Override | ||
public Optional<String> getNextPageUrl(HttpResponse response) { | ||
switch (policy.resolve(response)) { | ||
case PROCESS: | ||
return delegate.getNextPageUrl(response); | ||
case SKIP: | ||
return Optional.empty(); | ||
case FAIL: | ||
default: | ||
throw new IllegalStateException(String.format("Policy failed for response code: %s, body: %s", response.getCode(), ofNullable(response.getBody()).map(String::new).orElse(""))); | ||
} | ||
} | ||
Comment on lines
+70
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The purpose of the What's the reasoning to fit this functionality here? isn't it unrelated to the purpose of the class? |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,6 +39,7 @@ | |
import static java.util.stream.Collectors.toMap; | ||
import static org.apache.kafka.common.config.ConfigDef.Importance.HIGH; | ||
import static org.apache.kafka.common.config.ConfigDef.Importance.MEDIUM; | ||
import static org.apache.kafka.common.config.ConfigDef.Importance.LOW; | ||
import static org.apache.kafka.common.config.ConfigDef.Type.STRING; | ||
|
||
@Getter | ||
|
@@ -49,12 +50,14 @@ public class JacksonRecordParserConfig extends AbstractConfig { | |
private static final String ITEM_KEY_POINTER = "http.response.record.key.pointer"; | ||
private static final String ITEM_TIMESTAMP_POINTER = "http.response.record.timestamp.pointer"; | ||
private static final String ITEM_OFFSET_VALUE_POINTER = "http.response.record.offset.pointer"; | ||
private static final String NEXT_PAGE_POINTER = "http.response.next.page.pointer"; | ||
|
||
private final JsonPointer recordsPointer; | ||
private final List<JsonPointer> keyPointer; | ||
private final JsonPointer valuePointer; | ||
private final Optional<JsonPointer> timestampPointer; | ||
private final Map<String, JsonPointer> offsetPointers; | ||
private final Optional<JsonPointer> nextPagePointer; | ||
|
||
JacksonRecordParserConfig(Map<String, ?> originals) { | ||
super(config(), originals); | ||
|
@@ -65,6 +68,7 @@ public class JacksonRecordParserConfig extends AbstractConfig { | |
offsetPointers = breakDownMap(getString(ITEM_OFFSET_VALUE_POINTER)).entrySet().stream() | ||
.map(entry -> new SimpleEntry<>(entry.getKey(), compile(entry.getValue()))) | ||
.collect(toMap(Entry::getKey, Entry::getValue)); | ||
nextPagePointer = ofNullable(getString(NEXT_PAGE_POINTER)).map(JsonPointer::compile); | ||
} | ||
|
||
public static ConfigDef config() { | ||
|
@@ -73,6 +77,7 @@ public static ConfigDef config() { | |
.define(ITEM_POINTER, STRING, "/", HIGH, "Item JsonPointer") | ||
.define(ITEM_KEY_POINTER, STRING, null, HIGH, "Item Key JsonPointers") | ||
.define(ITEM_TIMESTAMP_POINTER, STRING, null, MEDIUM, "Item Timestamp JsonPointer") | ||
.define(ITEM_OFFSET_VALUE_POINTER, STRING, "", MEDIUM, "Item Offset JsonPointers"); | ||
.define(ITEM_OFFSET_VALUE_POINTER, STRING, "", MEDIUM, "Item Offset JsonPointers") | ||
.define(NEXT_PAGE_POINTER, STRING, "/next", LOW, "Pointer for next page"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's probably not fair to assume something as arbitrary |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,7 @@ | |
import org.apache.kafka.common.Configurable; | ||
|
||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.function.Function; | ||
import java.util.stream.Stream; | ||
|
||
|
@@ -45,6 +46,10 @@ public class JacksonResponseRecordParser implements Configurable { | |
|
||
private JsonPointer recordsPointer; | ||
|
||
private Optional<JsonPointer> nextPagePointer; | ||
|
||
private JsonNode jsonBody; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This class is meant to be stateless, we shouldn't be keeping this body around here. |
||
|
||
public JacksonResponseRecordParser() { | ||
this(new JacksonRecordParser(), new JacksonSerializer(new ObjectMapper())); | ||
} | ||
|
@@ -61,14 +66,21 @@ public void configure(Map<String, ?> settings) { | |
|
||
Stream<JacksonRecord> getRecords(byte[] body) { | ||
|
||
JsonNode jsonBody = serializer.deserialize(body); | ||
this.jsonBody = serializer.deserialize(body); | ||
|
||
Map<String, Object> responseOffset = getResponseOffset(jsonBody); | ||
|
||
return serializer.getArrayAt(jsonBody, recordsPointer) | ||
.map(jsonRecord -> toJacksonRecord(jsonRecord, responseOffset)); | ||
} | ||
|
||
Optional<String> getNextPageUrl(byte[] body) { | ||
return nextPagePointer.map(pointer -> | ||
serializer.checkIfNonNull(this.jsonBody, pointer) | ||
? serializer.getObjectAt(this.jsonBody, pointer).asText() | ||
: null); | ||
Comment on lines
+79
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this would belong better one level down at the serializer, by providing something like this: Optional<JsonNode> getObjectAtIfPresent(JsonNode node, JsonPointer pointer) That would mean you wouldn't need to expose |
||
} | ||
|
||
private Map<String, Object> getResponseOffset(JsonNode node) { | ||
return emptyMap(); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,13 +26,15 @@ | |
|
||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
|
||
@FunctionalInterface | ||
public interface HttpResponseParser extends Configurable { | ||
|
||
List<SourceRecord> parse(HttpResponse response); | ||
|
||
default void configure(Map<String, ?> map) { | ||
// Do nothing | ||
} | ||
|
||
Optional<String> getNextPageUrl(HttpResponse response); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The responsibility of this parser is to translate responses to records, this additional responsibility is not cohesive with it. In other words, we are breaking single responsibility here. If the new functionality were cohesive with the existing one, maybe we could consider raising the abstraction level of this interface to absorb it, but I don't think it is. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,13 +26,17 @@ | |
|
||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
|
||
@FunctionalInterface | ||
public interface KvRecordHttpResponseParser extends Configurable { | ||
|
||
List<KvRecord> parse(HttpResponse response); | ||
|
||
default void configure(Map<String, ?> map) { | ||
// Do nothing | ||
} | ||
|
||
Optional<String> getNextPageUrl(HttpResponse response); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as |
||
|
||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need this here?