embulk-input-zendesk 0.3.4 → 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/README.md +32 -2
  4. data/build.gradle +1 -1
  5. data/src/main/java/org/embulk/input/zendesk/RecordImporter.java +134 -0
  6. data/src/main/java/org/embulk/input/zendesk/ZendeskInputPlugin.java +182 -202
  7. data/src/main/java/org/embulk/input/zendesk/clients/ZendeskRestClient.java +54 -52
  8. data/src/main/java/org/embulk/input/zendesk/models/Target.java +3 -3
  9. data/src/main/java/org/embulk/input/zendesk/models/ZendeskException.java +1 -1
  10. data/src/main/java/org/embulk/input/zendesk/services/ZendeskCustomObjectService.java +110 -0
  11. data/src/main/java/org/embulk/input/zendesk/services/ZendeskNPSService.java +30 -0
  12. data/src/main/java/org/embulk/input/zendesk/services/ZendeskNormalServices.java +239 -0
  13. data/src/main/java/org/embulk/input/zendesk/services/ZendeskService.java +14 -0
  14. data/src/main/java/org/embulk/input/zendesk/services/ZendeskSupportAPIService.java +25 -83
  15. data/src/main/java/org/embulk/input/zendesk/services/ZendeskUserEventService.java +158 -0
  16. data/src/main/java/org/embulk/input/zendesk/stream/PagingSpliterator.java +40 -0
  17. data/src/main/java/org/embulk/input/zendesk/stream/paginator/sunshine/CustomObjectSpliterator.java +42 -0
  18. data/src/main/java/org/embulk/input/zendesk/stream/paginator/sunshine/SunshineSpliterator.java +66 -0
  19. data/src/main/java/org/embulk/input/zendesk/stream/paginator/sunshine/UserEventSpliterator.java +35 -0
  20. data/src/main/java/org/embulk/input/zendesk/stream/paginator/support/OrganizationSpliterator.java +13 -0
  21. data/src/main/java/org/embulk/input/zendesk/stream/paginator/support/SupportSpliterator.java +44 -0
  22. data/src/main/java/org/embulk/input/zendesk/stream/paginator/support/UserSpliterator.java +13 -0
  23. data/src/main/java/org/embulk/input/zendesk/utils/ZendeskConstants.java +13 -1
  24. data/src/main/java/org/embulk/input/zendesk/utils/ZendeskDateUtils.java +22 -11
  25. data/src/main/java/org/embulk/input/zendesk/utils/ZendeskUtils.java +52 -114
  26. data/src/test/java/org/embulk/input/zendesk/TestRecordImporter.java +114 -0
  27. data/src/test/java/org/embulk/input/zendesk/TestZendeskInputPlugin.java +184 -99
  28. data/src/test/java/org/embulk/input/zendesk/clients/TestZendeskRestClient.java +6 -20
  29. data/src/test/java/org/embulk/input/zendesk/services/TestZendeskCustomObjectService.java +161 -0
  30. data/src/test/java/org/embulk/input/zendesk/services/TestZendeskNPSService.java +56 -0
  31. data/src/test/java/org/embulk/input/zendesk/services/TestZendeskNormalService.java +189 -0
  32. data/src/test/java/org/embulk/input/zendesk/services/TestZendeskSupportAPIService.java +18 -60
  33. data/src/test/java/org/embulk/input/zendesk/services/TestZendeskUserEventService.java +158 -0
  34. data/src/test/java/org/embulk/input/zendesk/utils/TestZendeskDateUtils.java +50 -2
  35. data/src/test/java/org/embulk/input/zendesk/utils/TestZendeskUtil.java +0 -138
  36. data/src/test/java/org/embulk/input/zendesk/utils/ZendeskTestHelper.java +16 -0
  37. data/src/test/resources/config/nps.yml +29 -0
  38. data/src/test/resources/config/object_records.yml +24 -0
  39. data/src/test/resources/config/relationship_records.yml +23 -0
  40. data/src/test/resources/config/user_events.yml +29 -0
  41. data/src/test/resources/data/duplicate_user.json +0 -0
  42. data/src/test/resources/data/empty_result.json +7 -0
  43. data/src/test/resources/data/expected/user_events_column.json +40 -0
  44. data/src/test/resources/data/object_records.json +30 -0
  45. data/src/test/resources/data/organization.json +39 -0
  46. data/src/test/resources/data/relationship_records.json +57 -0
  47. data/src/test/resources/data/scores.json +21 -0
  48. data/src/test/resources/data/scores_share_same_time_with_next_page.json +35 -0
  49. data/src/test/resources/data/scores_share_same_time_without_next_page.json +35 -0
  50. data/src/test/resources/data/simple_organization.json +23 -0
  51. data/src/test/resources/data/simple_user.json +50 -0
  52. data/src/test/resources/data/simple_user_event.json +19 -0
  53. data/src/test/resources/data/ticket_events_share_same_time_with_next_page.json +279 -0
  54. data/src/test/resources/data/ticket_events_share_same_time_without_next_page.json +279 -0
  55. data/src/test/resources/data/ticket_events_updated_by_system_records.json +279 -0
  56. data/src/test/resources/data/ticket_share_same_time_with_next_page.json +232 -0
  57. data/src/test/resources/data/ticket_share_same_time_without_next_page.json +232 -0
  58. data/src/test/resources/data/ticket_with_updated_by_system_records.json +187 -0
  59. data/src/test/resources/data/user_event.json +19 -0
  60. data/src/test/resources/data/user_event_contain_latter_create_at.json +19 -0
  61. data/src/test/resources/data/user_event_multiple.json +33 -0
  62. metadata +46 -5
  63. data/src/main/java/org/embulk/input/zendesk/utils/ZendeskValidatorUtils.java +0 -79
  64. data/src/test/java/org/embulk/input/zendesk/utils/TestZendeskValidatorUtils.java +0 -130
@@ -7,7 +7,6 @@ import com.google.common.collect.ImmutableMap;
7
7
  import com.google.common.collect.ImmutableMap.Builder;
8
8
  import com.google.common.util.concurrent.RateLimiter;
9
9
  import com.google.common.util.concurrent.Uninterruptibles;
10
-
11
10
  import org.apache.http.Header;
12
11
  import org.apache.http.HttpResponse;
13
12
  import org.apache.http.HttpStatus;
@@ -26,23 +25,21 @@ import org.embulk.spi.DataException;
26
25
  import org.embulk.spi.Exec;
27
26
  import org.embulk.spi.util.RetryExecutor;
28
27
  import org.slf4j.Logger;
29
- import static org.apache.http.HttpHeaders.AUTHORIZATION;
30
- import static org.apache.http.protocol.HTTP.CONTENT_TYPE;
31
- import static org.embulk.spi.util.RetryExecutor.retryExecutor;
32
28
 
33
29
  import java.io.IOException;
34
-
35
30
  import java.util.concurrent.TimeUnit;
36
31
 
32
+ import static org.apache.http.HttpHeaders.AUTHORIZATION;
33
+ import static org.apache.http.protocol.HTTP.CONTENT_TYPE;
34
+ import static org.embulk.spi.util.RetryExecutor.retryExecutor;
35
+
37
36
  public class ZendeskRestClient
38
37
  {
39
38
  private static final int CONNECTION_TIME_OUT = 240000;
40
39
 
41
40
  private static final Logger logger = Exec.getLogger(ZendeskRestClient.class);
42
-
43
- private static RateLimiter rateLimiter;
44
-
45
41
  private static final ObjectMapper objectMapper = new ObjectMapper();
42
+ private static RateLimiter rateLimiter;
46
43
 
47
44
  static {
48
45
  objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@@ -52,35 +49,37 @@ public class ZendeskRestClient
52
49
  {
53
50
  }
54
51
 
55
- public String doGet(String url, PluginTask task, boolean isPreview)
52
+ public String doGet(final String url, final PluginTask task, final boolean isPreview)
56
53
  {
57
54
  try {
58
55
  return retryExecutor().withRetryLimit(task.getRetryLimit())
59
56
  .withInitialRetryWait(task.getRetryInitialWaitSec() * 1000)
60
57
  .withMaxRetryWait(task.getMaxRetryWaitSec() * 1000)
61
- .runInterruptible(new RetryExecutor.Retryable<String>() {
58
+ .runInterruptible(new RetryExecutor.Retryable<String>()
59
+ {
62
60
  @Override
63
- public String call() throws Exception
61
+ public String call()
62
+ throws Exception
64
63
  {
65
64
  return sendGetRequest(url, task);
66
65
  }
67
66
 
68
67
  @Override
69
- public boolean isRetryableException(Exception exception)
68
+ public boolean isRetryableException(final Exception exception)
70
69
  {
71
70
  if (exception instanceof ZendeskException) {
72
- int statusCode = ((ZendeskException) exception).getStatusCode();
71
+ final int statusCode = ((ZendeskException) exception).getStatusCode();
73
72
  return isResponseStatusToRetry(statusCode, exception.getMessage(), ((ZendeskException) exception).getRetryAfter(), isPreview);
74
73
  }
75
74
  return false;
76
75
  }
77
76
 
78
77
  @Override
79
- public void onRetry(Exception exception, int retryCount, int retryLimit, int retryWait)
78
+ public void onRetry(final Exception exception, final int retryCount, final int retryLimit, final int retryWait)
80
79
  {
81
80
  if (exception instanceof ZendeskException) {
82
- int retryAfter = ((ZendeskException) exception).getRetryAfter();
83
- String message;
81
+ final int retryAfter = ((ZendeskException) exception).getRetryAfter();
82
+ final String message;
84
83
  if (retryAfter > 0 && retryAfter > (retryWait / 1000)) {
85
84
  message = String
86
85
  .format("Retrying '%d'/'%d' after '%d' seconds. HTTP status code: '%s'",
@@ -100,7 +99,7 @@ public class ZendeskRestClient
100
99
  }
101
100
  }
102
101
  else {
103
- String message = String
102
+ final String message = String
104
103
  .format("Retrying '%d'/'%d' after '%d' seconds. Message: '%s'",
105
104
  retryCount, retryLimit,
106
105
  retryWait / 1000,
@@ -110,15 +109,15 @@ public class ZendeskRestClient
110
109
  }
111
110
 
112
111
  @Override
113
- public void onGiveup(Exception firstException, Exception lastException)
112
+ public void onGiveup(final Exception firstException, final Exception lastException)
114
113
  {
115
114
  }
116
115
  });
117
116
  }
118
- catch (RetryExecutor.RetryGiveupException | InterruptedException e) {
117
+ catch (final RetryExecutor.RetryGiveupException | InterruptedException e) {
119
118
  if (e instanceof RetryExecutor.RetryGiveupException && e.getCause() != null && e.getCause() instanceof ZendeskException) {
120
119
  throw new ConfigException("Status: '" + ((ZendeskException) (e.getCause())).getStatusCode() + "', error message: '" + e.getCause().getMessage() + "'",
121
- e.getCause());
120
+ e.getCause());
122
121
  }
123
122
  throw new ConfigException(e);
124
123
  }
@@ -127,27 +126,34 @@ public class ZendeskRestClient
127
126
  @VisibleForTesting
128
127
  protected HttpClient createHttpClient()
129
128
  {
130
- RequestConfig config = RequestConfig.custom()
129
+ final RequestConfig config = RequestConfig.custom()
131
130
  .setConnectTimeout(CONNECTION_TIME_OUT)
132
131
  .setConnectionRequestTimeout(CONNECTION_TIME_OUT)
133
132
  .build();
134
133
  return HttpClientBuilder.create().setDefaultRequestConfig(config).build();
135
134
  }
136
135
 
137
- private String sendGetRequest(final String url, final PluginTask task) throws ZendeskException
136
+ private String sendGetRequest(final String url, final PluginTask task)
137
+ throws ZendeskException
138
138
  {
139
139
  try {
140
- HttpClient client = createHttpClient();
141
- HttpRequestBase request = createGetRequest(url, task);
140
+ final HttpClient client = createHttpClient();
141
+ final HttpRequestBase request = createGetRequest(url, task);
142
+
143
+ if (rateLimiter != null) {
144
+ rateLimiter.acquire();
145
+ }
142
146
  logger.info(">>> {}{}", request.getURI().getPath(),
143
147
  request.getURI().getQuery() != null ? "?" + request.getURI().getQuery() : "");
144
- HttpResponse response = client.execute(request);
145
- getRateLimiter(response).acquire();
146
- int statusCode = response.getStatusLine().getStatusCode();
148
+ final HttpResponse response = client.execute(request);
149
+ if (rateLimiter == null) {
150
+ initRateLimiter(response);
151
+ }
152
+ final int statusCode = response.getStatusLine().getStatusCode();
147
153
  if (statusCode != HttpStatus.SC_OK) {
148
154
  if (statusCode == ZendeskConstants.HttpStatus.TOO_MANY_REQUEST || statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR
149
155
  || statusCode == HttpStatus.SC_SERVICE_UNAVAILABLE) {
150
- Header retryHeader = response.getFirstHeader("Retry-After");
156
+ final Header retryHeader = response.getFirstHeader("Retry-After");
151
157
  if (retryHeader != null) {
152
158
  throw new ZendeskException(statusCode, EntityUtils.toString(response.getEntity()), Integer.parseInt(retryHeader.getValue()));
153
159
  }
@@ -156,12 +162,12 @@ public class ZendeskRestClient
156
162
  }
157
163
  return EntityUtils.toString(response.getEntity());
158
164
  }
159
- catch (IOException ex) {
165
+ catch (final IOException ex) {
160
166
  throw new ZendeskException(-1, ex.getMessage(), 0);
161
167
  }
162
168
  }
163
169
 
164
- private boolean isResponseStatusToRetry(final int status, final String message, int retryAfter, final boolean isPreview)
170
+ private boolean isResponseStatusToRetry(final int status, final String message, final int retryAfter, final boolean isPreview)
165
171
  {
166
172
  if (status == HttpStatus.SC_NOT_FOUND) {
167
173
  // 404 would be returned e.g. ticket comments are empty (on fetchRelatedObjects method)
@@ -204,23 +210,23 @@ public class ZendeskRestClient
204
210
  return true;
205
211
  }
206
212
 
207
- private HttpRequestBase createGetRequest(String url, PluginTask task)
213
+ private HttpRequestBase createGetRequest(final String url, final PluginTask task)
208
214
  {
209
- HttpGet request = new HttpGet(url);
210
- ImmutableMap<String, String> headers = buildAuthHeader(task);
215
+ final HttpGet request = new HttpGet(url);
216
+ final ImmutableMap<String, String> headers = buildAuthHeader(task);
211
217
  headers.forEach(request::setHeader);
212
218
  return request;
213
219
  }
214
220
 
215
- private ImmutableMap<String, String> buildAuthHeader(PluginTask task)
221
+ private ImmutableMap<String, String> buildAuthHeader(final PluginTask task)
216
222
  {
217
- Builder<String, String> builder = new Builder<>();
223
+ final Builder<String, String> builder = new Builder<>();
218
224
  builder.put(AUTHORIZATION, buildCredential(task));
219
225
  addCommonHeader(builder, task);
220
226
  return builder.build();
221
227
  }
222
228
 
223
- private String buildCredential(PluginTask task)
229
+ private String buildCredential(final PluginTask task)
224
230
  {
225
231
  switch (task.getAuthenticationMethod()) {
226
232
  case BASIC:
@@ -233,7 +239,7 @@ public class ZendeskRestClient
233
239
  return "";
234
240
  }
235
241
 
236
- private void addCommonHeader(final Builder<String, String> builder, PluginTask task)
242
+ private void addCommonHeader(final Builder<String, String> builder, final PluginTask task)
237
243
  {
238
244
  task.getAppMarketPlaceIntegrationName().ifPresent(s -> builder.put(ZendeskConstants.Header.ZENDESK_MARKETPLACE_NAME, s));
239
245
  task.getAppMarketPlaceAppId().ifPresent(s -> builder.put(ZendeskConstants.Header.ZENDESK_MARKETPLACE_APP_ID, s));
@@ -242,28 +248,24 @@ public class ZendeskRestClient
242
248
  builder.put(CONTENT_TYPE, ZendeskConstants.Header.APPLICATION_JSON);
243
249
  }
244
250
 
245
- private RateLimiter getRateLimiter(final HttpResponse response)
251
+ private synchronized void initRateLimiter(final HttpResponse response)
246
252
  {
247
- if (rateLimiter == null) {
248
- rateLimiter = initRateLimiter(response);
249
- }
250
- return rateLimiter;
251
- }
252
-
253
- private static synchronized RateLimiter initRateLimiter(final HttpResponse response)
254
- {
255
- double permits = 0.0;
256
253
  if (response.containsHeader("x-rate-limit")) {
257
- String rateLimit = response.getFirstHeader("x-rate-limit").getValue();
254
+ double permits = 0.0;
255
+ final String rateLimit = response.getFirstHeader("x-rate-limit").getValue();
258
256
  try {
259
257
  permits = Double.parseDouble(rateLimit);
260
258
  }
261
- catch (NumberFormatException e) {
259
+ catch (final NumberFormatException e) {
262
260
  throw new DataException("Error when parse x-rate-limit: '" + response.getFirstHeader("x-rate-limit").getValue() + "'");
263
261
  }
264
- permits = permits / 60;
262
+
263
+ if (permits > 0) {
264
+ permits = permits / 60;
265
+ logger.info("Permits per second " + permits);
266
+
267
+ rateLimiter = RateLimiter.create(permits);
268
+ }
265
269
  }
266
- logger.info("Permits per second " + permits);
267
- return RateLimiter.create(permits);
268
270
  }
269
271
  }
@@ -10,9 +10,9 @@ public enum Target
10
10
  /** For ticket_metrics - we fetch by using include metric_sets with ticket target
11
11
  * so the jsonName is different comparing to the target name
12
12
  */
13
- TICKETS("tickets"), USERS("users"), ORGANIZATIONS("organizations"),
14
- TICKET_EVENTS("ticket_events"), TICKET_METRICS("metric_sets"),
15
- TICKET_FIELDS("ticket_fields"), TICKET_FORMS("ticket_forms");
13
+ TICKETS("tickets"), USERS("users"), ORGANIZATIONS("organizations"), TICKET_EVENTS("ticket_events"),
14
+ TICKET_METRICS("metric_sets"), TICKET_FIELDS("ticket_fields"), TICKET_FORMS("ticket_forms"),
15
+ RECIPIENTS("recipients"), SCORES("responses"), OBJECT_RECORDS("data"), RELATIONSHIP_RECORDS("data"), USER_EVENTS("data");
16
16
 
17
17
  String jsonName;
18
18
 
@@ -6,7 +6,7 @@ public class ZendeskException extends Exception
6
6
  private final int statusCode;
7
7
  private final int retryAfter;
8
8
 
9
- public ZendeskException(int statusCode, String message, int retryAfter)
9
+ public ZendeskException(final int statusCode, final String message, final int retryAfter)
10
10
  {
11
11
  super(message);
12
12
  this.statusCode = statusCode;
@@ -0,0 +1,110 @@
1
+ package org.embulk.input.zendesk.services;
2
+
3
+ import com.fasterxml.jackson.databind.JsonNode;
4
+ import com.fasterxml.jackson.databind.ObjectMapper;
5
+ import com.google.common.annotations.VisibleForTesting;
6
+ import com.google.common.base.Preconditions;
7
+ import org.apache.http.HttpStatus;
8
+ import org.embulk.config.ConfigException;
9
+ import org.embulk.config.TaskReport;
10
+ import org.embulk.input.zendesk.RecordImporter;
11
+ import org.embulk.input.zendesk.ZendeskInputPlugin;
12
+ import org.embulk.input.zendesk.clients.ZendeskRestClient;
13
+ import org.embulk.input.zendesk.models.Target;
14
+ import org.embulk.input.zendesk.models.ZendeskException;
15
+ import org.embulk.input.zendesk.stream.paginator.sunshine.CustomObjectSpliterator;
16
+ import org.embulk.input.zendesk.utils.ZendeskConstants;
17
+ import org.embulk.input.zendesk.utils.ZendeskUtils;
18
+ import org.embulk.spi.Exec;
19
+
20
+ import java.util.Collections;
21
+ import java.util.List;
22
+ import java.util.Optional;
23
+ import java.util.stream.Collectors;
24
+ import java.util.stream.StreamSupport;
25
+
26
+ public class ZendeskCustomObjectService implements ZendeskService
27
+ {
28
+ protected ZendeskInputPlugin.PluginTask task;
29
+
30
+ private ZendeskRestClient zendeskRestClient;
31
+
32
+ public ZendeskCustomObjectService(final ZendeskInputPlugin.PluginTask task)
33
+ {
34
+ this.task = task;
35
+ }
36
+
37
+ public boolean isSupportIncremental()
38
+ {
39
+ return false;
40
+ }
41
+
42
+ @Override
43
+ public TaskReport addRecordToImporter(final int taskIndex, final RecordImporter recordImporter)
44
+ {
45
+ final List<String> paths = getListPathByTarget();
46
+
47
+ paths.parallelStream().forEach(path -> StreamSupport.stream(new CustomObjectSpliterator(path, getZendeskRestClient(), task, Exec.isPreview()), !Exec.isPreview())
48
+ .forEach(recordImporter::addRecord));
49
+
50
+ return Exec.newTaskReport();
51
+ }
52
+
53
+ @Override
54
+ public JsonNode getDataFromPath(final String path, final int page, final boolean isPreview, final long startTime)
55
+ {
56
+ Preconditions.checkArgument(isPreview, "IsPreview should be true to use this method");
57
+
58
+ Optional<String> response = Optional.empty();
59
+ final List<String> paths = path.isEmpty()
60
+ ? getListPathByTarget()
61
+ : Collections.singletonList(path);
62
+
63
+ for (final String temp : paths) {
64
+ try {
65
+ response = Optional.ofNullable(getZendeskRestClient().doGet(temp, task, true));
66
+
67
+ if (response.isPresent()) {
68
+ break;
69
+ }
70
+ }
71
+ catch (final ConfigException e) {
72
+ // Sometimes we get 404 when having invalid endpoint, so ignore when we get 404
73
+ if (!(e.getCause() instanceof ZendeskException && ((ZendeskException) e.getCause()).getStatusCode() == HttpStatus.SC_NOT_FOUND)) {
74
+ throw e;
75
+ }
76
+ }
77
+ }
78
+
79
+ return response.map(ZendeskUtils::parseJsonObject).orElse(new ObjectMapper().createObjectNode());
80
+ }
81
+
82
+ @VisibleForTesting
83
+ protected ZendeskRestClient getZendeskRestClient()
84
+ {
85
+ if (zendeskRestClient == null) {
86
+ zendeskRestClient = new ZendeskRestClient();
87
+ }
88
+ return zendeskRestClient;
89
+ }
90
+
91
+ private List<String> getListPathByTarget()
92
+ {
93
+ return task.getTarget().equals(Target.OBJECT_RECORDS)
94
+ ? task.getObjectTypes().stream().map(this::buildPath).collect(Collectors.toList())
95
+ : task.getRelationshipTypes().stream().map(this::buildPath).collect(Collectors.toList());
96
+ }
97
+
98
+ private String buildPath(final String value)
99
+ {
100
+ final String perPage = Exec.isPreview() ? "1" : "1000";
101
+
102
+ return ZendeskUtils.getURIBuilder(task.getLoginUrl())
103
+ .setPath(task.getTarget().equals(Target.OBJECT_RECORDS)
104
+ ? ZendeskConstants.Url.API_OBJECT_RECORD
105
+ : ZendeskConstants.Url.API_RELATIONSHIP_RECORD)
106
+ .setParameter("type", value)
107
+ .setParameter("per_page", perPage)
108
+ .toString();
109
+ }
110
+ }
@@ -0,0 +1,30 @@
1
+ package org.embulk.input.zendesk.services;
2
+
3
+ import org.embulk.input.zendesk.ZendeskInputPlugin;
4
+ import org.embulk.input.zendesk.utils.ZendeskConstants;
5
+ import org.embulk.input.zendesk.utils.ZendeskUtils;
6
+
7
+ public class ZendeskNPSService extends ZendeskNormalServices
8
+ {
9
+ public ZendeskNPSService(final ZendeskInputPlugin.PluginTask task)
10
+ {
11
+ super(task);
12
+ }
13
+
14
+ public boolean isSupportIncremental()
15
+ {
16
+ return true;
17
+ }
18
+
19
+ @Override
20
+ protected String buildURI(final int page, final long startTime)
21
+ {
22
+ return ZendeskUtils.getURIBuilder(task.getLoginUrl())
23
+ .setPath(ZendeskConstants.Url.API_NPS_INCREMENTAL
24
+ + "/"
25
+ + task.getTarget().getJsonName()
26
+ + ".json")
27
+ .setParameter(ZendeskConstants.Field.START_TIME, String.valueOf(startTime))
28
+ .toString();
29
+ }
30
+ }
@@ -0,0 +1,239 @@
1
+ package org.embulk.input.zendesk.services;
2
+
3
+ import com.fasterxml.jackson.databind.JsonNode;
4
+ import com.fasterxml.jackson.databind.node.ObjectNode;
5
+ import com.google.common.annotations.VisibleForTesting;
6
+ import com.google.common.base.Throwables;
7
+ import org.apache.http.HttpStatus;
8
+ import org.apache.http.client.utils.URIBuilder;
9
+ import org.embulk.config.ConfigException;
10
+ import org.embulk.config.TaskReport;
11
+ import org.embulk.input.zendesk.RecordImporter;
12
+ import org.embulk.input.zendesk.ZendeskInputPlugin;
13
+ import org.embulk.input.zendesk.clients.ZendeskRestClient;
14
+ import org.embulk.input.zendesk.models.ZendeskException;
15
+ import org.embulk.input.zendesk.utils.ZendeskConstants;
16
+ import org.embulk.input.zendesk.utils.ZendeskDateUtils;
17
+ import org.embulk.input.zendesk.utils.ZendeskUtils;
18
+ import org.embulk.spi.Exec;
19
+ import org.slf4j.Logger;
20
+
21
+ import java.time.Instant;
22
+
23
+ import java.util.Iterator;
24
+ import java.util.Set;
25
+ import java.util.concurrent.ConcurrentHashMap;
26
+ import java.util.concurrent.LinkedBlockingQueue;
27
+ import java.util.concurrent.ThreadPoolExecutor;
28
+ import java.util.concurrent.TimeUnit;
29
+
30
+ public abstract class ZendeskNormalServices implements ZendeskService
31
+ {
32
+ private static final Logger logger = Exec.getLogger(ZendeskNormalServices.class);
33
+
34
+ protected ZendeskInputPlugin.PluginTask task;
35
+
36
+ private ZendeskRestClient zendeskRestClient;
37
+
38
+ protected ZendeskNormalServices(final ZendeskInputPlugin.PluginTask task)
39
+ {
40
+ this.task = task;
41
+ }
42
+
43
+ public TaskReport addRecordToImporter(final int taskIndex, final RecordImporter recordImporter)
44
+ {
45
+ TaskReport taskReport = Exec.newTaskReport();
46
+
47
+ if (isSupportIncremental()) {
48
+ importDataForIncremental(task, recordImporter, taskReport);
49
+ }
50
+ else {
51
+ importDataForNonIncremental(task, taskIndex, recordImporter);
52
+ }
53
+
54
+ return taskReport;
55
+ }
56
+
57
+ public JsonNode getDataFromPath(String path, final int page, final boolean isPreview, final long startTime)
58
+ {
59
+ if (path.isEmpty()) {
60
+ path = buildURI(page, startTime);
61
+ }
62
+
63
+ final String response = getZendeskRestClient().doGet(path, task, isPreview);
64
+ return ZendeskUtils.parseJsonObject(response);
65
+ }
66
+
67
+ protected abstract String buildURI(int page, long startTime);
68
+
69
+ @VisibleForTesting
70
+ protected ZendeskRestClient getZendeskRestClient()
71
+ {
72
+ if (zendeskRestClient == null) {
73
+ zendeskRestClient = new ZendeskRestClient();
74
+ }
75
+ return zendeskRestClient;
76
+ }
77
+
78
+ private void importDataForIncremental(final ZendeskInputPlugin.PluginTask task, final RecordImporter recordImporter, final TaskReport taskReport)
79
+ {
80
+ long startTime = 0;
81
+
82
+ if (task.getStartTime().isPresent()) {
83
+ startTime = ZendeskDateUtils.getStartTime(task.getStartTime().get());
84
+ }
85
+
86
+ // For incremental target, we will run in one task but split in multiple threads inside for data deduplication.
87
+ // Run with incremental will contain duplicated data.
88
+ ThreadPoolExecutor pool = null;
89
+ try {
90
+ final Set<String> knownIds = ConcurrentHashMap.newKeySet();
91
+ pool = new ThreadPoolExecutor(
92
+ 10, 100, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>()
93
+ );
94
+
95
+ while (true) {
96
+ int recordCount = 0;
97
+
98
+ // Page argument isn't used in incremental API so we just set it to 0
99
+ final JsonNode result = getDataFromPath("", 0, false, startTime);
100
+ final Iterator<JsonNode> iterator = ZendeskUtils.getListRecords(result, task.getTarget().getJsonName());
101
+
102
+ int numberOfRecords = 0;
103
+ if (result.has(ZendeskConstants.Field.COUNT)) {
104
+ numberOfRecords = result.get(ZendeskConstants.Field.COUNT).asInt();
105
+ }
106
+
107
+ while (iterator.hasNext()) {
108
+ final JsonNode recordJsonNode = iterator.next();
109
+
110
+ if (isUpdatedBySystem(recordJsonNode, startTime)) {
111
+ continue;
112
+ }
113
+
114
+ if (task.getDedup()) {
115
+ final String recordID = recordJsonNode.get(ZendeskConstants.Field.ID).asText();
116
+
117
+ // add success -> no duplicate
118
+ if (!knownIds.add(recordID)) {
119
+ continue;
120
+ }
121
+ }
122
+
123
+ pool.submit(() -> fetchSubResourceAndAddToImporter(recordJsonNode, task, recordImporter));
124
+ recordCount++;
125
+ if (Exec.isPreview()) {
126
+ return;
127
+ }
128
+ }
129
+
130
+ logger.info("Fetched '{}' records from start_time '{}'", recordCount, startTime);
131
+
132
+ // https://developer.zendesk.com/rest_api/docs/support/incremental_export#pagination
133
+ // When there are more than 1000 records share the same time stamp, the count > 1000
134
+ long apiEndTime = result.get(ZendeskConstants.Field.END_TIME).asLong();
135
+ startTime = startTime == apiEndTime
136
+ ? apiEndTime + 1
137
+ : apiEndTime;
138
+
139
+ if (numberOfRecords < ZendeskConstants.Misc.MAXIMUM_RECORDS_INCREMENTAL) {
140
+ break;
141
+ }
142
+ }
143
+
144
+ if (!Exec.isPreview()) {
145
+ storeStartTimeForConfigDiff(taskReport, startTime);
146
+ }
147
+ }
148
+ finally {
149
+ if (pool != null) {
150
+ pool.shutdown();
151
+ try {
152
+ pool.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
153
+ }
154
+ catch (final InterruptedException e) {
155
+ logger.warn("Error when wait pool to finish");
156
+ throw Throwables.propagate(e);
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ private void storeStartTimeForConfigDiff(final TaskReport taskReport, final long resultEndTime)
163
+ {
164
+ if (task.getIncremental()) {
165
+ // resultEndTime = 0 mean no records, we should store now time for next run
166
+ if (resultEndTime == 0) {
167
+ taskReport.set(ZendeskConstants.Field.START_TIME, Instant.now().getEpochSecond());
168
+ }
169
+ else {
170
+ // NOTE: start_time compared as "=>", not ">".
171
+ // If we will use end_time for next start_time, we got the same records that are fetched
172
+ // end_time + 1 is workaround for that
173
+ taskReport.set(ZendeskConstants.Field.START_TIME, resultEndTime + 1);
174
+ }
175
+ }
176
+ }
177
+
178
+ private void fetchSubResourceAndAddToImporter(final JsonNode jsonNode, final ZendeskInputPlugin.PluginTask task, final RecordImporter recordImporter)
179
+ {
180
+ task.getIncludes().forEach(include -> {
181
+ final String relatedObjectName = include.trim();
182
+
183
+ final URIBuilder uriBuilder = ZendeskUtils.getURIBuilder(task.getLoginUrl())
184
+ .setPath(ZendeskConstants.Url.API
185
+ + "/" + task.getTarget().toString()
186
+ + "/" + jsonNode.get(ZendeskConstants.Field.ID).asText()
187
+ + "/" + relatedObjectName + ".json");
188
+ try {
189
+ final JsonNode result = getDataFromPath(uriBuilder.toString(), 0, false, 0);
190
+ if (result != null && result.has(relatedObjectName)) {
191
+ ((ObjectNode) jsonNode).set(include, result.get(relatedObjectName));
192
+ }
193
+ }
194
+ catch (final ConfigException e) {
195
+ // Sometimes we get 404 when having invalid endpoint, so ignore when we get 404 InvalidEndpoint
196
+ if (!(e.getCause() instanceof ZendeskException && ((ZendeskException) e.getCause()).getStatusCode() == HttpStatus.SC_NOT_FOUND)) {
197
+ throw e;
198
+ }
199
+ }
200
+ });
201
+
202
+ recordImporter.addRecord(jsonNode);
203
+ }
204
+
205
+ private boolean isUpdatedBySystem(final JsonNode recordJsonNode, final long startTime)
206
+ {
207
+ /*
208
+ * https://developer.zendesk.com/rest_api/docs/core/incremental_export#excluding-system-updates
209
+ * "generated_timestamp" will be updated when Zendesk internal changing
210
+ * "updated_at" will be updated when ticket data was changed
211
+ * start_time for query parameter will be processed on Zendesk with generated_timestamp,
212
+ * but it was calculated by record' updated_at time.
213
+ * So the doesn't changed record from previous import would be appear by Zendesk internal changes.
214
+ */
215
+ if (recordJsonNode.has(ZendeskConstants.Field.GENERATED_TIMESTAMP) && recordJsonNode.has(ZendeskConstants.Field.UPDATED_AT)) {
216
+ final String recordUpdatedAtTime = recordJsonNode.get(ZendeskConstants.Field.UPDATED_AT).asText();
217
+ final long recordUpdatedAtToEpochSecond = ZendeskDateUtils.isoToEpochSecond(recordUpdatedAtTime);
218
+
219
+ return recordUpdatedAtToEpochSecond <= startTime;
220
+ }
221
+
222
+ return false;
223
+ }
224
+
225
+ private void importDataForNonIncremental(final ZendeskInputPlugin.PluginTask task, final int taskIndex, RecordImporter recordImporter)
226
+ {
227
+ // Page start from 1 => page = taskIndex + 1
228
+ final JsonNode result = getDataFromPath("", taskIndex + 1, false, 0);
229
+ final Iterator<JsonNode> iterator = ZendeskUtils.getListRecords(result, task.getTarget().getJsonName());
230
+
231
+ while (iterator.hasNext()) {
232
+ fetchSubResourceAndAddToImporter(iterator.next(), task, recordImporter);
233
+
234
+ if (Exec.isPreview()) {
235
+ break;
236
+ }
237
+ }
238
+ }
239
+ }