embulk-input-marketo 0.5.6 → 0.5.7.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.github/PULL_REQUEST_TEMPLATE.md +37 -0
  3. data/.gitignore +11 -2
  4. data/.travis.yml +5 -45
  5. data/LICENSE.txt +21 -0
  6. data/README.md +14 -65
  7. data/build.gradle +102 -0
  8. data/config/checkstyle/checkstyle.xml +128 -0
  9. data/config/checkstyle/default.xml +108 -0
  10. data/gradle/wrapper/gradle-wrapper.jar +0 -0
  11. data/gradle/wrapper/gradle-wrapper.properties +6 -0
  12. data/gradlew +169 -0
  13. data/gradlew.bat +84 -0
  14. data/lib/embulk/input/marketo.rb +3 -0
  15. data/settings.gradle +1 -0
  16. data/src/main/java/org/embulk/input/marketo/CsvTokenizer.java +677 -0
  17. data/src/main/java/org/embulk/input/marketo/MarketoInputPlugin.java +15 -0
  18. data/src/main/java/org/embulk/input/marketo/MarketoInputPluginDelegate.java +77 -0
  19. data/src/main/java/org/embulk/input/marketo/MarketoService.java +30 -0
  20. data/src/main/java/org/embulk/input/marketo/MarketoServiceImpl.java +176 -0
  21. data/src/main/java/org/embulk/input/marketo/MarketoUtils.java +172 -0
  22. data/src/main/java/org/embulk/input/marketo/delegate/ActivityBulkExtractInputPlugin.java +63 -0
  23. data/src/main/java/org/embulk/input/marketo/delegate/CampaignInputPlugin.java +67 -0
  24. data/src/main/java/org/embulk/input/marketo/delegate/LeadBulkExtractInputPlugin.java +61 -0
  25. data/src/main/java/org/embulk/input/marketo/delegate/LeadWithListInputPlugin.java +58 -0
  26. data/src/main/java/org/embulk/input/marketo/delegate/LeadWithProgramInputPlugin.java +56 -0
  27. data/src/main/java/org/embulk/input/marketo/delegate/MarketoBaseBulkExtractInputPlugin.java +260 -0
  28. data/src/main/java/org/embulk/input/marketo/delegate/MarketoBaseInputPluginDelegate.java +69 -0
  29. data/src/main/java/org/embulk/input/marketo/exception/MarketoAPIException.java +30 -0
  30. data/src/main/java/org/embulk/input/marketo/model/MarketoAccessTokenResponse.java +92 -0
  31. data/src/main/java/org/embulk/input/marketo/model/MarketoBulkExtractRequest.java +59 -0
  32. data/src/main/java/org/embulk/input/marketo/model/MarketoError.java +40 -0
  33. data/src/main/java/org/embulk/input/marketo/model/MarketoField.java +91 -0
  34. data/src/main/java/org/embulk/input/marketo/model/MarketoResponse.java +81 -0
  35. data/src/main/java/org/embulk/input/marketo/model/filter/DateRangeFilter.java +31 -0
  36. data/src/main/java/org/embulk/input/marketo/model/filter/ListFilter.java +10 -0
  37. data/src/main/java/org/embulk/input/marketo/model/filter/MarketoFilter.java +8 -0
  38. data/src/main/java/org/embulk/input/marketo/rest/MarketoBaseRestClient.java +226 -0
  39. data/src/main/java/org/embulk/input/marketo/rest/MarketoFileResponseEntityReader.java +69 -0
  40. data/src/main/java/org/embulk/input/marketo/rest/MarketoRESTEndpoint.java +44 -0
  41. data/src/main/java/org/embulk/input/marketo/rest/MarketoResponseJetty92EntityReader.java +88 -0
  42. data/src/main/java/org/embulk/input/marketo/rest/MarketoRestClient.java +332 -0
  43. data/src/main/java/org/embulk/input/marketo/rest/RecordPagingIterable.java +130 -0
  44. data/src/test/java/org/embulk/input/marketo/TestMarketoInputPlugin.java +5 -0
  45. data/src/test/java/org/embulk/input/marketo/rest/MarketoBaseRestClientTest.java +220 -0
  46. metadata +65 -222
  47. data/.ruby-version +0 -1
  48. data/.travis.yml.erb +0 -42
  49. data/Gemfile +0 -3
  50. data/LICENSE +0 -13
  51. data/Rakefile +0 -20
  52. data/embulk-input-marketo.gemspec +0 -28
  53. data/gemfiles/embulk-latest +0 -4
  54. data/gemfiles/template.erb +0 -4
  55. data/lib/embulk/input/marketo/activity_log.rb +0 -103
  56. data/lib/embulk/input/marketo/base.rb +0 -139
  57. data/lib/embulk/input/marketo/lead.rb +0 -143
  58. data/lib/embulk/input/marketo_api.rb +0 -22
  59. data/lib/embulk/input/marketo_api/soap/activity_log.rb +0 -103
  60. data/lib/embulk/input/marketo_api/soap/base.rb +0 -135
  61. data/lib/embulk/input/marketo_api/soap/lead.rb +0 -91
  62. data/test/activity_log_fixtures.rb +0 -216
  63. data/test/embulk/input/marketo/test_activity_log.rb +0 -444
  64. data/test/embulk/input/marketo/test_base.rb +0 -76
  65. data/test/embulk/input/marketo/test_lead.rb +0 -605
  66. data/test/embulk/input/marketo_api/soap/test_activity_log.rb +0 -154
  67. data/test/embulk/input/marketo_api/soap/test_base.rb +0 -96
  68. data/test/embulk/input/marketo_api/soap/test_lead.rb +0 -139
  69. data/test/embulk/input/test_marketo_api.rb +0 -28
  70. data/test/lead_fixtures.rb +0 -111
  71. data/test/mute_logger.rb +0 -7
  72. data/test/override_assert_raise.rb +0 -18
  73. data/test/prepare_embulk.rb +0 -15
  74. data/test/run-test.rb +0 -26
  75. data/test/savon_helper.rb +0 -17
@@ -0,0 +1,332 @@
1
+ package org.embulk.input.marketo.rest;
2
+
3
+ import com.fasterxml.jackson.core.JsonProcessingException;
4
+ import com.fasterxml.jackson.databind.node.ObjectNode;
5
+ import com.google.common.collect.ArrayListMultimap;
6
+ import com.google.common.collect.ImmutableMap;
7
+ import com.google.common.collect.Multimap;
8
+ import org.apache.commons.lang3.StringUtils;
9
+ import org.embulk.config.Config;
10
+ import org.embulk.config.ConfigDefault;
11
+ import org.embulk.config.Task;
12
+ import org.embulk.input.marketo.MarketoUtils;
13
+ import org.embulk.input.marketo.model.MarketoBulkExtractRequest;
14
+ import org.embulk.input.marketo.model.MarketoError;
15
+ import org.embulk.input.marketo.model.MarketoField;
16
+ import org.embulk.input.marketo.model.MarketoResponse;
17
+ import org.embulk.input.marketo.model.filter.DateRangeFilter;
18
+ import org.embulk.input.marketo.model.filter.ListFilter;
19
+ import org.embulk.input.marketo.model.filter.MarketoFilter;
20
+ import org.embulk.spi.DataException;
21
+ import org.embulk.spi.Exec;
22
+ import org.embulk.spi.type.Type;
23
+ import org.embulk.spi.type.Types;
24
+ import org.embulk.util.retryhelper.jetty92.Jetty92RetryHelper;
25
+ import org.slf4j.Logger;
26
+
27
+ import java.io.InputStream;
28
+ import java.text.SimpleDateFormat;
29
+ import java.util.ArrayList;
30
+ import java.util.Date;
31
+ import java.util.HashMap;
32
+ import java.util.List;
33
+ import java.util.Map;
34
+
35
+ /**
36
+ * Created by tai.khuu on 8/22/17.
37
+ */
38
+ public class MarketoRestClient extends MarketoBaseRestClient
39
+ {
40
+ private static final String BATCH_SIZE = "batchSize";
41
+
42
+ private static final String NEXT_PAGE_TOKEN = "nextPageToken";
43
+
44
+ private String endPoint;
45
+
46
+ private Integer batchSize;
47
+
48
+ private static final Logger LOGGER = Exec.getLogger(MarketoRestClient.class.getCanonicalName());
49
+
50
+ private static final Map<String, Type> TYPE_MAPPING = new ImmutableMap.Builder<String, Type>()
51
+ .put("datetime", Types.TIMESTAMP)
52
+ .put("email", Types.STRING)
53
+ .put("float", Types.DOUBLE)
54
+ .put("integer", Types.LONG)
55
+ .put("formula", Types.STRING)
56
+ .put("percent", Types.LONG)
57
+ .put("url", Types.STRING)
58
+ .put("phone", Types.STRING)
59
+ .put("textarea", Types.STRING)
60
+ .put("text", Types.STRING)
61
+ .put("string", Types.STRING)
62
+ .put("score", Types.LONG)
63
+ .put("boolean", Types.BOOLEAN)
64
+ .put("currency", Types.DOUBLE)
65
+ .put("date", Types.TIMESTAMP)
66
+ .put("reference", Types.STRING)
67
+ .build();
68
+
69
+ public interface PluginTask extends Task
70
+ {
71
+
72
+ @Config("account_id")
73
+ String getAccountId();
74
+
75
+ @Config("client_secret")
76
+ String getClientSecret();
77
+
78
+ @Config("client_id")
79
+ String getClientId();
80
+
81
+ @Config("marketo_limit_interval_milis")
82
+ @ConfigDefault("20000")
83
+ Integer getMarketoLimitIntervalMilis();
84
+
85
+ @Config("batch_size")
86
+ @ConfigDefault("300")
87
+ Integer getBatchSize();
88
+
89
+ void setBatchSize(Integer batchSize);
90
+ }
91
+
92
+ public MarketoRestClient(PluginTask task, Jetty92RetryHelper retryHelper)
93
+ {
94
+ this("https://" + task.getAccountId() + ".mktorest.com", "https://" + task.getAccountId() + ".mktorest.com/identity", task.getClientId(), task.getClientSecret(), task.getBatchSize(), task.getMarketoLimitIntervalMilis(), retryHelper);
95
+ }
96
+
97
+ public MarketoRestClient(String endPoint, String identityEndPoint, String clientId, String clientSecret, Integer batchSize, int marketoLimitIntervalMilis, Jetty92RetryHelper retryHelper)
98
+ {
99
+ super(identityEndPoint, clientId, clientSecret, marketoLimitIntervalMilis, retryHelper);
100
+ this.endPoint = endPoint;
101
+ this.batchSize = batchSize;
102
+ }
103
+
104
+ public List<MarketoField> describeLead()
105
+ {
106
+ MarketoResponse<ObjectNode> jsonResponse = doGet(endPoint + MarketoRESTEndpoint.DESCRIBE_LEAD.getEndpoint(), null, null, new MarketoResponseJetty92EntityReader<ObjectNode>(READ_TIMEOUT_MILLIS));
107
+ List<MarketoField> marketoFields = new ArrayList<>();
108
+ List<ObjectNode> fields = jsonResponse.getResult();
109
+ for (int i = 0; i < fields.size(); i++) {
110
+ ObjectNode field = fields.get(i);
111
+ String dataType = field.get("dataType").asText();
112
+ ObjectNode restField = (ObjectNode) field.get("rest");
113
+ String name = restField.get("name").asText();
114
+ marketoFields.add(new MarketoField(name, dataType));
115
+ }
116
+ return marketoFields;
117
+ }
118
+
119
+ private Type getType(String dataType)
120
+ {
121
+ return TYPE_MAPPING.containsKey(dataType.toLowerCase()) ? TYPE_MAPPING.get(dataType.toLowerCase()) : Types.STRING;
122
+ }
123
+
124
+ public String createLeadBulkExtract(Date startTime, Date endTime, List<String> extractFields)
125
+ {
126
+ SimpleDateFormat timeFormat = new SimpleDateFormat(MarketoUtils.MARKETO_DATE_SIMPLE_DATE_FORMAT);
127
+ MarketoBulkExtractRequest marketoBulkExtractRequest = new MarketoBulkExtractRequest();
128
+ marketoBulkExtractRequest.setFields(extractFields);
129
+ marketoBulkExtractRequest.setFormat("CSV");
130
+ Map<String, MarketoFilter> filterMap = new HashMap<>();
131
+ DateRangeFilter dateRangeFilter = new DateRangeFilter();
132
+ dateRangeFilter.setStartAt(timeFormat.format(startTime));
133
+ dateRangeFilter.setEndAt(timeFormat.format(endTime));
134
+ filterMap.put("createdAt", dateRangeFilter);
135
+ marketoBulkExtractRequest.setFilter(filterMap);
136
+ return sendCreateBulkExtractRequest(marketoBulkExtractRequest, MarketoRESTEndpoint.CREATE_LEAD_EXTRACT);
137
+ }
138
+
139
+ public String createActitvityExtract(Date startTime, Date endTime, List<String> activityTypes)
140
+ {
141
+ SimpleDateFormat timeFormat = new SimpleDateFormat(MarketoUtils.MARKETO_DATE_SIMPLE_DATE_FORMAT);
142
+ MarketoBulkExtractRequest marketoBulkExtractRequest = new MarketoBulkExtractRequest();
143
+ marketoBulkExtractRequest.setFormat("CSV");
144
+ Map<String, MarketoFilter> filterMap = new HashMap<>();
145
+ DateRangeFilter dateRangeFilter = new DateRangeFilter();
146
+ dateRangeFilter.setStartAt(timeFormat.format(startTime));
147
+ dateRangeFilter.setEndAt(timeFormat.format(endTime));
148
+ filterMap.put("createdAt", dateRangeFilter);
149
+ if (activityTypes != null) {
150
+ ListFilter activitiesTypeFilter = new ListFilter();
151
+ activitiesTypeFilter.addAll(activityTypes);
152
+ filterMap.put("activities", activitiesTypeFilter);
153
+ }
154
+ marketoBulkExtractRequest.setFilter(filterMap);
155
+ return sendCreateBulkExtractRequest(marketoBulkExtractRequest, MarketoRESTEndpoint.CREATE_ACTIVITY_EXTRACT);
156
+ }
157
+
158
+ public String sendCreateBulkExtractRequest(MarketoBulkExtractRequest request, MarketoRESTEndpoint endpoint)
159
+ {
160
+ MarketoResponse<ObjectNode> marketoResponse = null;
161
+ try {
162
+ marketoResponse = doPost(endPoint + endpoint.getEndpoint(), null, null, OBJECT_MAPPER.writeValueAsString(request), new MarketoResponseJetty92EntityReader<ObjectNode>(READ_TIMEOUT_MILLIS));
163
+ }
164
+ catch (JsonProcessingException e) {
165
+ LOGGER.error("Encounter exception when deserialize bulk extract request", e);
166
+ throw new DataException("Can't create bulk extract");
167
+ }
168
+ if (!marketoResponse.isSuccess()) {
169
+ MarketoError marketoError = marketoResponse.getErrors().get(0);
170
+ throw new DataException(marketoError.getCode() + ": " + marketoError.getMessage());
171
+ }
172
+ ObjectNode objectNode = marketoResponse.getResult().get(0);
173
+ return objectNode.get("exportId").asText();
174
+ }
175
+
176
+ public void startLeadBulkExtract(String exportId)
177
+ {
178
+ startBulkExtract(MarketoRESTEndpoint.START_LEAD_EXPORT_JOB, exportId);
179
+ }
180
+
181
+ public void startActitvityBulkExtract(String exportId)
182
+ {
183
+ startBulkExtract(MarketoRESTEndpoint.START_ACTIVITY_EXPORT_JOB, exportId);
184
+ }
185
+
186
+ private void startBulkExtract(MarketoRESTEndpoint marketoRESTEndpoint, String exportId)
187
+ {
188
+ MarketoResponse<ObjectNode> marketoResponse = doPost(endPoint + marketoRESTEndpoint.getEndpoint(
189
+ new ImmutableMap.Builder<String, String>().put("export_id", exportId).build()), null, null, null,
190
+ new MarketoResponseJetty92EntityReader<ObjectNode>(READ_TIMEOUT_MILLIS));
191
+ if (!marketoResponse.isSuccess()) {
192
+ MarketoError error = marketoResponse.getErrors().get(0);
193
+ throw new DataException(String.format("Can't start job for export Job id : %s, error code: %s, error message: %s", exportId, error.getCode(), error.getMessage()));
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Wait for lead bulk extract job
199
+ * Will block and wait until job status switch to complete
200
+ * If job run logger than bulk job timeout then will stop and throw exception
201
+ * If job status is failed or cancel will also throw exception
202
+ *
203
+ * @param exportId
204
+ * @throws InterruptedException
205
+ */
206
+ public void waitLeadExportJobComplete(String exportId, int pollingInterval, int waitTimeout) throws InterruptedException
207
+ {
208
+ waitExportJobComplete(MarketoRESTEndpoint.GET_LEAD_EXPORT_STATUS, exportId, pollingInterval, waitTimeout);
209
+ }
210
+
211
+ /**
212
+ * Wait for activites bulk extract job
213
+ * Will block and wait until job status switch to complete
214
+ * If job run logger than bulk job timeout then will stop and throw exception
215
+ * If job status is failed or cancel will also throw exception
216
+ *
217
+ * @param exportId
218
+ * @throws InterruptedException
219
+ */
220
+ public void waitActitvityExportJobComplete(String exportId, int pollingInterval, int waitTimeout) throws InterruptedException
221
+ {
222
+ waitExportJobComplete(MarketoRESTEndpoint.GET_ACTIVITY_EXPORT_STATUS, exportId, pollingInterval, waitTimeout);
223
+ }
224
+
225
+ private void waitExportJobComplete(MarketoRESTEndpoint marketoRESTEndpoint, String exportId, int pollingInterval, int waitTimeout) throws InterruptedException
226
+ {
227
+ long waitTime = 0;
228
+ long now = System.currentTimeMillis();
229
+ while (true) {
230
+ MarketoResponse<ObjectNode> marketoResponse = doGet(this.endPoint + marketoRESTEndpoint.getEndpoint(
231
+ new ImmutableMap.Builder<String, String>().put("export_id", exportId).build()), null, null, new MarketoResponseJetty92EntityReader<ObjectNode>(READ_TIMEOUT_MILLIS));
232
+ if (marketoResponse.isSuccess()) {
233
+ ObjectNode objectNode = marketoResponse.getResult().get(0);
234
+ String status = objectNode.get("status").asText();
235
+ if (status == null) {
236
+ throw new DataException("Can't get bulk extract status export job id: " + exportId);
237
+ }
238
+ LOGGER.info("Jobs [{}] status is [{}]", exportId, status);
239
+ switch (status) {
240
+ case "Completed":
241
+ return;
242
+ case "Failed":
243
+ throw new DataException("Bulk extract job failed exportId: " + exportId + " errorMessage: " + objectNode.get("errorMsg").asText());
244
+ case "Cancel":
245
+ throw new DataException("Bulk extract job canceled, exportId: " + exportId);
246
+ }
247
+ }
248
+ Thread.sleep(pollingInterval * 1000);
249
+ waitTime = waitTime + (System.currentTimeMillis() - now);
250
+ if (waitTime >= (waitTimeout * 1000)) {
251
+ throw new DataException("Job timeout exception, exportJob: " + exportId + ", run longer than " + waitTimeout + " seconds");
252
+ }
253
+ }
254
+ }
255
+
256
+ public InputStream getLeadBulkExtractResult(String exportId)
257
+ {
258
+ return getBulkExtractResult(MarketoRESTEndpoint.GET_LEAD_EXPORT_RESULT, exportId);
259
+ }
260
+
261
+ public InputStream getActivitiesBulkExtractResult(String exportId)
262
+ {
263
+ return getBulkExtractResult(MarketoRESTEndpoint.GET_ACTIVITY_EXPORT_RESULT, exportId);
264
+ }
265
+
266
+ private InputStream getBulkExtractResult(MarketoRESTEndpoint endpoint, String exportId)
267
+ {
268
+ InputStream fileStream = doGet(this.endPoint + endpoint.getEndpoint(new ImmutableMap.Builder().put("export_id", exportId).build()), null, null, new MarketoFileResponseEntityReader(READ_TIMEOUT_MILLIS));
269
+ return fileStream;
270
+ }
271
+
272
+ public RecordPagingIterable<ObjectNode> getLists()
273
+ {
274
+ return getRecordWithPagination(endPoint + MarketoRESTEndpoint.GET_LISTS.getEndpoint(), null, ObjectNode.class);
275
+ }
276
+
277
+ public RecordPagingIterable<ObjectNode> getPrograms()
278
+ {
279
+ return getRecordWithPagination(endPoint + MarketoRESTEndpoint.GET_PROGRAMS.getEndpoint(), null, ObjectNode.class);
280
+ }
281
+
282
+ public RecordPagingIterable<ObjectNode> getLeadsByProgram(String programId, List<String> fieldNames)
283
+ {
284
+ Multimap<String, String> multimap = ArrayListMultimap.create();
285
+ multimap.put("fields", StringUtils.join(fieldNames, ","));
286
+ return getRecordWithPagination(endPoint + MarketoRESTEndpoint.GET_LEADS_BY_PROGRAM.getEndpoint(new ImmutableMap.Builder().put("program_id", programId).build()), multimap, ObjectNode.class);
287
+ }
288
+
289
+ public RecordPagingIterable<ObjectNode> getLeadsByList(String listId, List<String> fieldNames)
290
+ {
291
+ Multimap<String, String> multimap = ArrayListMultimap.create();
292
+ multimap.put("fields", StringUtils.join(fieldNames, ","));
293
+ return getRecordWithPagination(endPoint + MarketoRESTEndpoint.GET_LEADS_BY_LIST.getEndpoint(new ImmutableMap.Builder().put("list_id", listId).build()), multimap, ObjectNode.class);
294
+ }
295
+
296
+ public RecordPagingIterable<ObjectNode> getCampaign()
297
+ {
298
+ return getRecordWithPagination(endPoint + MarketoRESTEndpoint.GET_CAMPAIGN.getEndpoint(), null, ObjectNode.class);
299
+ }
300
+
301
+ private <T> RecordPagingIterable<T> getRecordWithPagination(final String endPoint, final Multimap<String, String> parameters, final Class<T> recordClass)
302
+ {
303
+ return new RecordPagingIterable<>(new RecordPagingIterable.PagingFunction<RecordPagingIterable.MarketoPage<T>>()
304
+ {
305
+ @Override
306
+ public RecordPagingIterable.MarketoPage<T> getNextPage(RecordPagingIterable.MarketoPage<T> currentPage)
307
+ {
308
+ Multimap<String, String> params = ArrayListMultimap.create();
309
+ params.put(NEXT_PAGE_TOKEN, currentPage.getNextPageToken());
310
+ return gettMarketoPage(params);
311
+ }
312
+
313
+ @Override
314
+ public RecordPagingIterable.MarketoPage<T> getFirstPage()
315
+ {
316
+ return gettMarketoPage(null);
317
+ }
318
+ private RecordPagingIterable.MarketoPage<T> gettMarketoPage(Multimap<String, String> params)
319
+ {
320
+ if (params == null) {
321
+ params = ArrayListMultimap.create();
322
+ }
323
+ params.put(BATCH_SIZE, String.valueOf(batchSize));
324
+ if (parameters != null) {
325
+ params.putAll(parameters);
326
+ }
327
+ MarketoResponse<T> marketoResponse = doGet(endPoint, null, params, new MarketoResponseJetty92EntityReader<>(READ_TIMEOUT_MILLIS, recordClass));
328
+ return new RecordPagingIterable.MarketoPage<>(marketoResponse.getResult(), marketoResponse.getNextPageToken(), marketoResponse.isMoreResult());
329
+ }
330
+ });
331
+ }
332
+ }
@@ -0,0 +1,130 @@
1
+ package org.embulk.input.marketo.rest;
2
+
3
+ import java.util.Iterator;
4
+ import java.util.List;
5
+ import java.util.NoSuchElementException;
6
+
7
+ /**
8
+ * Record Iterable class that will go through Marketo Paging
9
+ * Warning this iterator implementation do not cached page due to reduce memory usage. So iterate through a
10
+ * RecordIterate multiple time is not recommended since it will sent query to Marketo on every call.
11
+ * Created by tai.khuu on 9/5/17.
12
+ */
13
+ public class RecordPagingIterable<T> implements Iterable<T>
14
+ {
15
+ private PagingFunction pagingFunction;
16
+
17
+ public RecordPagingIterable(PagingFunction pagingFunction)
18
+ {
19
+ this.pagingFunction = pagingFunction;
20
+ }
21
+
22
+ @Override
23
+ public Iterator<T> iterator()
24
+ {
25
+ return this.new RecordIterator();
26
+ }
27
+
28
+ private class RecordIterator implements Iterator<T>
29
+ {
30
+ Page currentPage;
31
+ private Iterator<T> currentIterator;
32
+
33
+ public RecordIterator()
34
+ {
35
+ }
36
+
37
+ @Override
38
+ public boolean hasNext()
39
+ {
40
+ if (currentPage == null) {
41
+ currentPage = pagingFunction.getFirstPage();
42
+ this.currentIterator = currentPage.getRecords().iterator();
43
+ }
44
+ return currentIterator.hasNext() || currentPage.hasNext;
45
+ }
46
+
47
+ @Override
48
+ public T next()
49
+ {
50
+ if (!hasNext()) {
51
+ throw new NoSuchElementException("Call next on an empty iterator");
52
+ }
53
+ if (!currentIterator.hasNext()) {
54
+ currentPage = pagingFunction.getNextPage(currentPage);
55
+ currentIterator = currentPage.getRecords().iterator();
56
+ }
57
+ return currentIterator.next();
58
+ }
59
+
60
+ @Override
61
+ public void remove()
62
+ {
63
+ throw new UnsupportedOperationException("RecordIterator not support remove");
64
+ }
65
+ }
66
+
67
+ public interface PagingFunction<P extends Page>
68
+ {
69
+ P getNextPage(P currentPage);
70
+ /**
71
+ * All implementation must make sure calling get first page multiple time should always return.
72
+ * @return P
73
+ */
74
+ P getFirstPage();
75
+ }
76
+
77
+ public static class Page<T>
78
+ {
79
+ private Iterable<T> records;
80
+
81
+ private boolean hasNext;
82
+
83
+ public Page(Iterable<T> records, boolean hasNext)
84
+ {
85
+ this.records = records;
86
+ this.hasNext = hasNext;
87
+ }
88
+
89
+ public Iterable<T> getRecords()
90
+ {
91
+ return records;
92
+ }
93
+
94
+ public void setRecords(List<T> records)
95
+ {
96
+ this.records = records;
97
+ }
98
+
99
+ public boolean isHasNext()
100
+ {
101
+ return hasNext;
102
+ }
103
+
104
+ public void setHasNext(boolean hasNext)
105
+ {
106
+ this.hasNext = hasNext;
107
+ }
108
+ }
109
+
110
+ public static class MarketoPage<T> extends Page<T>
111
+ {
112
+ private String nextPageToken;
113
+
114
+ public MarketoPage(Iterable<T> records, String nextPageToken, boolean moreResult)
115
+ {
116
+ super(records, moreResult);
117
+ this.nextPageToken = nextPageToken;
118
+ }
119
+
120
+ public String getNextPageToken()
121
+ {
122
+ return nextPageToken;
123
+ }
124
+
125
+ public void setNextPageToken(String nextPageToken)
126
+ {
127
+ this.nextPageToken = nextPageToken;
128
+ }
129
+ }
130
+ }