embulk-input-zendesk 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 87550faf658f946b3304fc3e19db5903dea08cc0
4
- data.tar.gz: be6b8435c7650e8113ed7a36757e61ba0b4ff986
3
+ metadata.gz: 2dab4909dba68349742e03eef6be0ff02014caf1
4
+ data.tar.gz: 05bee8746fae83639eeb0daacb4b38ac5f765868
5
5
  SHA512:
6
- metadata.gz: cc547390d1c61746c3bca7c6e6483da976f0aadb9fb0fdd389a94fea0f79f237192edb39e55a523d3d5fb685355210f225b6b65014c37b83bf421c101575a3a3
7
- data.tar.gz: 2b029eb64f6c10bb082aa00c132a2e1dfe2df86c30151b6a133c9460df7f84c94a06bfa48d1e134f43e16c0052effbc64358a4b881d62b17367d39da7f3b5101
6
+ metadata.gz: 0b0831ce1c1a40d1d9a3b2b3e8a73d8a2fa14a6e5144d264e1e210f2289c092fed6778c7ccea823fcf4464cc8b8ef61182306426730f4a82c85a50b262783bdf
7
+ data.tar.gz: e1706af5499ff07076761b0aa591920abf867a2a95d38c4b5e18a47dd61e73ecb653cf5f3d4cca8beb631ddf3f6b3de9018ebb92fecf8558a4f6e3713c4b0715
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ ## 0.3.5 - 2019-06-03
2
+ * [enhancement] Add new targets #58 [#58](https://github.com/treasure-data/embulk-input-zendesk/pull/58)
3
+
1
4
  ## 0.3.4 - 2019-04-11
2
5
  * [enhancement] Add new time format #56 [#56](https://github.com/treasure-data/embulk-input-zendesk/pull/56)
3
6
 
data/README.md CHANGED
@@ -22,7 +22,7 @@ Required Embulk version >= 0.9.6.
22
22
 
23
23
  - **login_url**: Login URL for Zendesk (string, required)
24
24
  - **auth_method**: `basic`, `token`, or `oauth`. For more detail on [zendesk document](https://developer.zendesk.com/rest_api/docs/core/introduction#security-and-authentication). (string, required)
25
- - **target**: Which export Zendesk resource. Currently supported are `tickets`, `ticket_events`, `users`, `organizations`, `ticket_fields`, `ticket_forms` or `ticket_metrics`. (string, required)
25
+ - **target**: Which export Zendesk resource. Currently supported are `tickets`, `ticket_events`, `users`, `organizations`, `ticket_fields`, `ticket_forms`, `ticket_metrics`, `scores`, `recipients`, `object_records`, `relationship_records` or `user_events`. (string, required)
26
26
  - **includes**: Will fetch sub resources. For example, ticket has ticket_audits, ticket_comments. See below example config. (array, default: `[]`)
27
27
  - **username**: The user name a.k.a. email. Required if `auth_method` is `basic` or `token`. (string, default: `null`)
28
28
  - **password**: Password. required if `auth_method` is `basic`. (string, default: `null`)
@@ -36,7 +36,12 @@ Required Embulk version >= 0.9.6.
36
36
  - **app_marketplace_integration_name**: Invisible to user, only requires to be a part of the Zendesk Apps Marketplace. This should be used to name of the integration.
37
37
  - **app_marketplace_org_id**: Invisible to user, only requires to be a part of the Zendesk Apps Marketplace. This should be the Organization ID for your organization from the new developer portal.
38
38
  - **app_marketplace_app_id**: Invisible to user, only requires to be a part of the Zendesk Apps Marketplace. This is the “App ID” that will be assigned to you when you submit your app.
39
-
39
+ - **object_types**: List custom object types, required if `target` is `object_records`.
40
+ - **relationship_types**: List custom relationship types, required if `target` is `relationship_records`.
41
+ - **profile_source**: Profile source of user event, required if `target` is `user_events`.
42
+ - **user_event_source**: Source of user event, required if `target` is `user_events`.
43
+ - **user_event_type**: Type of user event, required if `target` is `user_events`.
44
+
40
45
  ## Example
41
46
 
42
47
  ```yaml
@@ -59,3 +64,28 @@ in:
59
64
  ```
60
65
  $ ./gradlew package
61
66
  ```
67
+
68
+
69
+ @Config("object_types")
70
+ @ConfigDefault("[]")
71
+ List<String> getObjectTypes();
72
+
73
+ @Config("relationship_types")
74
+ @ConfigDefault("[]")
75
+ List<String> getRelationshipTypes();
76
+
77
+ @Config("profile_source")
78
+ @ConfigDefault("null")
79
+ Optional<String> getProfileSource();
80
+
81
+ @Config("end_time")
82
+ @ConfigDefault("null")
83
+ Optional<String> getEndTime();
84
+
85
+ @Config("user_event_type")
86
+ @ConfigDefault("null")
87
+ Optional<String> getUserEventType();
88
+
89
+ @Config("user_event_source")
90
+ @ConfigDefault("null")
91
+ Optional<String> getUserEventSource();
data/build.gradle CHANGED
@@ -15,7 +15,7 @@ configurations {
15
15
  provided
16
16
  }
17
17
 
18
- version = "0.3.4"
18
+ version = "0.3.5"
19
19
 
20
20
  sourceCompatibility = 1.8
21
21
  targetCompatibility = 1.8
@@ -0,0 +1,134 @@
1
+ package org.embulk.input.zendesk;
2
+
3
+ import com.fasterxml.jackson.databind.JsonNode;
4
+ import org.embulk.input.zendesk.utils.ZendeskDateUtils;
5
+ import org.embulk.input.zendesk.utils.ZendeskUtils;
6
+ import org.embulk.spi.Column;
7
+ import org.embulk.spi.ColumnVisitor;
8
+ import org.embulk.spi.Exec;
9
+ import org.embulk.spi.PageBuilder;
10
+ import org.embulk.spi.Schema;
11
+ import org.embulk.spi.json.JsonParser;
12
+ import org.embulk.spi.time.Timestamp;
13
+ import org.slf4j.Logger;
14
+
15
+ import java.util.function.Function;
16
+
17
+ public class RecordImporter
18
+ {
19
+ private Schema schema;
20
+ private PageBuilder pageBuilder;
21
+
22
+ private static final Logger logger = Exec.getLogger(RecordImporter.class);
23
+
24
+ public RecordImporter(Schema schema, PageBuilder pageBuilder)
25
+ {
26
+ this.schema = schema;
27
+ this.pageBuilder = pageBuilder;
28
+ }
29
+
30
+ public synchronized void addRecord(final JsonNode record)
31
+ {
32
+ schema.visitColumns(new ColumnVisitor()
33
+ {
34
+ @Override
35
+ public void jsonColumn(final Column column)
36
+ {
37
+ final JsonNode data = record.get(column.getName());
38
+
39
+ setColumn(column, data, (value) -> {
40
+ pageBuilder.setJson(column, new JsonParser().parse(value.toString()));
41
+ return null;
42
+ });
43
+ }
44
+
45
+ @Override
46
+ public void stringColumn(final Column column)
47
+ {
48
+ final JsonNode data = record.get(column.getName());
49
+
50
+ setColumn(column, data, (value) -> {
51
+ pageBuilder.setString(column, value.asText());
52
+ return null;
53
+ });
54
+ }
55
+
56
+ @Override
57
+ public void timestampColumn(final Column column)
58
+ {
59
+ final JsonNode data = record.get(column.getName());
60
+ setColumn(column, data, (value) -> {
61
+ final Timestamp timestamp = getTimestampValue(value.asText());
62
+ if (timestamp == null) {
63
+ pageBuilder.setNull(column);
64
+ }
65
+ else {
66
+ pageBuilder.setTimestamp(column, timestamp);
67
+ }
68
+ return null;
69
+ });
70
+ }
71
+
72
+ @Override
73
+ public void booleanColumn(final Column column)
74
+ {
75
+ final JsonNode data = record.get(column.getName());
76
+
77
+ setColumn(column, data, (value) -> {
78
+ pageBuilder.setBoolean(column, value.asBoolean());
79
+ return null;
80
+ });
81
+ }
82
+
83
+ @Override
84
+ public void longColumn(final Column column)
85
+ {
86
+ final JsonNode data = record.get(column.getName());
87
+
88
+ setColumn(column, data, (value) -> {
89
+ pageBuilder.setLong(column, value.asLong());
90
+ return null;
91
+ });
92
+ }
93
+
94
+ @Override
95
+ public void doubleColumn(final Column column)
96
+ {
97
+ final JsonNode data = record.get(column.getName());
98
+
99
+ setColumn(column, data, (value) -> {
100
+ pageBuilder.setDouble(column, value.asDouble());
101
+ return null;
102
+ });
103
+ }
104
+
105
+ private void setColumn(final Column column, final JsonNode data, final Function<JsonNode, Void> setter)
106
+ {
107
+ if (ZendeskUtils.isNull(data)) {
108
+ pageBuilder.setNull(column);
109
+ return;
110
+ }
111
+ setter.apply(data);
112
+ }
113
+ });
114
+
115
+ pageBuilder.addRecord();
116
+ }
117
+
118
+ /*
119
+ * For getting the timestamp value of the node
120
+ * Sometime if the parser could not parse the value then return null
121
+ * */
122
+ private Timestamp getTimestampValue(final String value)
123
+ {
124
+ Timestamp result = null;
125
+ try {
126
+ final long timeStamp = ZendeskDateUtils.isoToEpochSecond(value);
127
+ result = Timestamp.ofEpochSecond(timeStamp);
128
+ }
129
+ catch (final Exception e) {
130
+ logger.warn("Error when parse time stamp data " + value);
131
+ }
132
+ return result;
133
+ }
134
+ }
@@ -5,10 +5,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
5
5
  import com.fasterxml.jackson.databind.node.ArrayNode;
6
6
  import com.fasterxml.jackson.databind.node.ObjectNode;
7
7
  import com.google.common.annotations.VisibleForTesting;
8
- import com.google.common.base.Throwables;
9
8
  import com.google.common.collect.ImmutableList;
10
9
 
11
- import org.apache.http.HttpStatus;
12
10
  import org.embulk.config.Config;
13
11
  import org.embulk.config.ConfigDefault;
14
12
  import org.embulk.config.ConfigDiff;
@@ -20,14 +18,15 @@ import org.embulk.config.TaskSource;
20
18
  import org.embulk.exec.GuessExecutor;
21
19
  import org.embulk.input.zendesk.models.AuthenticationMethod;
22
20
  import org.embulk.input.zendesk.models.Target;
23
- import org.embulk.input.zendesk.models.ZendeskException;
21
+ import org.embulk.input.zendesk.services.ZendeskCustomObjectService;
22
+ import org.embulk.input.zendesk.services.ZendeskNPSService;
23
+ import org.embulk.input.zendesk.services.ZendeskService;
24
24
  import org.embulk.input.zendesk.services.ZendeskSupportAPIService;
25
+ import org.embulk.input.zendesk.services.ZendeskUserEventService;
25
26
  import org.embulk.input.zendesk.utils.ZendeskConstants;
26
27
  import org.embulk.input.zendesk.utils.ZendeskDateUtils;
27
28
  import org.embulk.input.zendesk.utils.ZendeskUtils;
28
- import org.embulk.input.zendesk.utils.ZendeskValidatorUtils;
29
29
  import org.embulk.spi.Buffer;
30
- import org.embulk.spi.DataException;
31
30
  import org.embulk.spi.Exec;
32
31
  import org.embulk.spi.InputPlugin;
33
32
  import org.embulk.spi.PageBuilder;
@@ -45,14 +44,10 @@ import java.time.Instant;
45
44
  import java.time.OffsetDateTime;
46
45
  import java.time.ZoneOffset;
47
46
  import java.time.format.DateTimeFormatter;
47
+ import java.util.Arrays;
48
48
  import java.util.Iterator;
49
49
  import java.util.List;
50
50
  import java.util.Optional;
51
- import java.util.Set;
52
- import java.util.concurrent.ConcurrentHashMap;
53
- import java.util.concurrent.LinkedBlockingQueue;
54
- import java.util.concurrent.ThreadPoolExecutor;
55
- import java.util.concurrent.TimeUnit;
56
51
  import java.util.regex.Pattern;
57
52
  import java.util.stream.Collectors;
58
53
  import java.util.stream.StreamSupport;
@@ -133,27 +128,54 @@ public class ZendeskInputPlugin implements InputPlugin
133
128
  @ConfigDefault("null")
134
129
  Optional<String> getAppMarketPlaceAppId();
135
130
 
131
+ @Config("object_types")
132
+ @ConfigDefault("[]")
133
+ List<String> getObjectTypes();
134
+
135
+ @Config("relationship_types")
136
+ @ConfigDefault("[]")
137
+ List<String> getRelationshipTypes();
138
+
139
+ @Config("profile_source")
140
+ @ConfigDefault("null")
141
+ Optional<String> getProfileSource();
142
+
143
+ @Config("end_time")
144
+ @ConfigDefault("null")
145
+ Optional<String> getEndTime();
146
+
147
+ @Config("user_event_type")
148
+ @ConfigDefault("null")
149
+ Optional<String> getUserEventType();
150
+
151
+ @Config("user_event_source")
152
+ @ConfigDefault("null")
153
+ Optional<String> getUserEventSource();
154
+
136
155
  @Config("columns")
137
156
  SchemaConfig getColumns();
138
157
  }
139
158
 
140
- private static final Logger logger = Exec.getLogger(ZendeskInputPlugin.class);
159
+ private ZendeskService zendeskService;
160
+
161
+ private RecordImporter recordImporter;
141
162
 
142
- private ZendeskSupportAPIService zendeskSupportAPIService;
163
+ private static final Logger logger = Exec.getLogger(ZendeskInputPlugin.class);
143
164
 
144
165
  @Override
145
166
  public ConfigDiff transaction(final ConfigSource config, final Control control)
146
167
  {
147
168
  final PluginTask task = config.loadConfig(PluginTask.class);
148
- ZendeskValidatorUtils.validateInputTask(task, getZendeskSupportAPIService(task));
169
+ validateInputTask(task);
149
170
  final Schema schema = task.getColumns().toSchema();
150
171
  int taskCount = 1;
151
172
 
152
173
  // For non-incremental target, we will split records based on number of pages. 100 records per page
153
174
  // In preview, run with taskCount = 1
154
- if (!ZendeskUtils.isSupportAPIIncremental(task.getTarget()) && !Exec.isPreview()) {
155
- final JsonNode result = getZendeskSupportAPIService(task).getData("", 0, false, 0);
156
- if (result.has(ZendeskConstants.Field.COUNT) && result.get(ZendeskConstants.Field.COUNT).isInt()) {
175
+ if (!Exec.isPreview() && !getZendeskService(task).isSupportIncremental() && getZendeskService(task) instanceof ZendeskSupportAPIService) {
176
+ final JsonNode result = getZendeskService(task).getDataFromPath("", 0, false, 0);
177
+ if (result.has(ZendeskConstants.Field.COUNT) && !result.get(ZendeskConstants.Field.COUNT).isNull()
178
+ && result.get(ZendeskConstants.Field.COUNT).isInt()) {
157
179
  taskCount = ZendeskUtils.numberToSplitWithHintingInTask(result.get(ZendeskConstants.Field.COUNT).asInt());
158
180
  }
159
181
  }
@@ -178,7 +200,7 @@ public class ZendeskInputPlugin implements InputPlugin
178
200
  {
179
201
  final PluginTask task = taskSource.loadTask(PluginTask.class);
180
202
  try (final PageBuilder pageBuilder = getPageBuilder(schema, output)) {
181
- final TaskReport taskReport = ingestServiceData(task, taskIndex, schema, pageBuilder);
203
+ final TaskReport taskReport = getZendeskService(task).addRecordToImporter(taskIndex, getRecordImporter(schema, pageBuilder));
182
204
  pageBuilder.finish();
183
205
  return taskReport;
184
206
  }
@@ -189,20 +211,10 @@ public class ZendeskInputPlugin implements InputPlugin
189
211
  {
190
212
  config.set("columns", new ObjectMapper().createArrayNode());
191
213
  final PluginTask task = config.loadConfig(PluginTask.class);
192
- ZendeskValidatorUtils.validateInputTask(task, getZendeskSupportAPIService(task));
214
+ validateInputTask(task);
193
215
  return Exec.newConfigDiff().set("columns", buildColumns(task));
194
216
  }
195
217
 
196
- @VisibleForTesting
197
- protected ZendeskSupportAPIService getZendeskSupportAPIService(final PluginTask task)
198
- {
199
- if (this.zendeskSupportAPIService == null) {
200
- this.zendeskSupportAPIService = new ZendeskSupportAPIService(task);
201
- }
202
- this.zendeskSupportAPIService.setTask(task);
203
- return this.zendeskSupportAPIService;
204
- }
205
-
206
218
  @VisibleForTesting
207
219
  protected PageBuilder getPageBuilder(final Schema schema, final PageOutput output)
208
220
  {
@@ -220,153 +232,26 @@ public class ZendeskInputPlugin implements InputPlugin
220
232
  taskReport.get(JsonNode.class, ZendeskConstants.Field.START_TIME).asLong()), ZoneOffset.UTC);
221
233
 
222
234
  configDiff.set(ZendeskConstants.Field.START_TIME,
223
- offsetDateTime.format(DateTimeFormatter.ofPattern(ZendeskConstants.Misc.RUBY_TIMESTAMP_FORMAT_INPUT)));
235
+ offsetDateTime.format(DateTimeFormatter.ofPattern(ZendeskConstants.Misc.RUBY_TIMESTAMP_FORMAT_INPUT)));
224
236
  }
225
237
  }
226
238
  return configDiff;
227
239
  }
228
240
 
229
- private TaskReport ingestServiceData(final PluginTask task, final int taskIndex,
230
- final Schema schema, final PageBuilder pageBuilder)
231
- {
232
- final TaskReport taskReport = Exec.newTaskReport();
233
-
234
- if (ZendeskUtils.isSupportAPIIncremental(task.getTarget())) {
235
- importDataForIncremental(task, schema, pageBuilder, taskReport);
236
- }
237
- else {
238
- importDataForNonIncremental(task, taskIndex, schema, pageBuilder);
239
- }
240
-
241
- return taskReport;
242
- }
243
-
244
- private void importDataForIncremental(final PluginTask task, final Schema schema,
245
- final PageBuilder pageBuilder, final TaskReport taskReport)
246
- {
247
- long startTime = 0;
248
-
249
- if (ZendeskUtils.isSupportAPIIncremental(task.getTarget()) && task.getStartTime().isPresent()) {
250
- startTime = ZendeskDateUtils.isoToEpochSecond(task.getStartTime().get());
251
- }
252
-
253
- // For incremental target, we will run in one task but split in multiple threads inside for data deduplication.
254
- // Run with incremental will contain duplicated data.
255
- ThreadPoolExecutor pool = null;
256
- try {
257
- Set<String> knownIds = ConcurrentHashMap.newKeySet();
258
- pool = new ThreadPoolExecutor(
259
- 10, 100, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>()
260
- );
261
-
262
- while (true) {
263
- int recordCount = 0;
264
-
265
- // Page argument isn't used in incremental API so we just set it to 0
266
- final JsonNode result = getZendeskSupportAPIService(task).getData("", 0, false, startTime);
267
- final Iterator<JsonNode> iterator = getListRecords(result, task.getTarget().getJsonName());
268
-
269
- int numberOfRecords = 0;
270
- if (result.has(ZendeskConstants.Field.COUNT)) {
271
- numberOfRecords = result.get(ZendeskConstants.Field.COUNT).asInt();
272
- }
273
-
274
- while (iterator.hasNext()) {
275
- final JsonNode recordJsonNode = iterator.next();
276
-
277
- if (isUpdatedBySystem(recordJsonNode, startTime)) {
278
- continue;
279
- }
280
-
281
- if (task.getDedup()) {
282
- String recordID = recordJsonNode.get(ZendeskConstants.Field.ID).asText();
283
- if (knownIds.contains(recordID)) {
284
- continue;
285
- }
286
- knownIds.add(recordID);
287
- }
288
-
289
- pool.submit(() -> fetchData(recordJsonNode, task, schema, pageBuilder));
290
- recordCount++;
291
- if (Exec.isPreview()) {
292
- return;
293
- }
294
- }
295
- logger.info("Fetched '{}' records from start_time '{}'", recordCount, startTime);
296
-
297
- if (task.getIncremental()) {
298
- if (result.has(ZendeskConstants.Field.END_TIME) && !result.get(ZendeskConstants.Field.END_TIME).isNull()
299
- && result.has(task.getTarget().getJsonName())) {
300
- // NOTE: start_time compared as "=>", not ">".
301
- // If we will use end_time for next start_time, we got the same record that is last fetched
302
- // end_time + 1 is workaround for that
303
- taskReport.set(ZendeskConstants.Field.START_TIME, result.get(ZendeskConstants.Field.END_TIME).asLong() + 1);
304
- }
305
- else {
306
- // Sometimes no record and no end_time fetched on the job, but we should generate start_time on config_diff.
307
- taskReport.set(ZendeskConstants.Field.START_TIME, Instant.now().getEpochSecond());
308
- }
309
- }
310
-
311
- if (numberOfRecords < ZendeskConstants.Misc.MAXIMUM_RECORDS_INCREMENTAL) {
312
- break;
313
- }
314
- else {
315
- startTime = result.get(ZendeskConstants.Field.END_TIME).asLong();
316
- }
317
- }
318
- }
319
- finally {
320
- if (pool != null) {
321
- pool.shutdown();
322
- try {
323
- pool.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
324
- }
325
- catch (final InterruptedException e) {
326
- logger.warn("Error when wait pool to finish");
327
- throw Throwables.propagate(e);
328
- }
329
- }
330
- }
331
- }
332
-
333
- private void importDataForNonIncremental(final PluginTask task, final int taskIndex, final Schema schema,
334
- final PageBuilder pageBuilder)
335
- {
336
- // Page start from 1 => page = taskIndex + 1
337
- final JsonNode result = getZendeskSupportAPIService(task).getData("", taskIndex + 1, false, 0);
338
- final Iterator<JsonNode> iterator = getListRecords(result, task.getTarget().getJsonName());
339
-
340
- while (iterator.hasNext()) {
341
- fetchData(iterator.next(), task, schema, pageBuilder);
342
-
343
- if (Exec.isPreview()) {
344
- break;
345
- }
346
- }
347
- }
348
-
349
- private Iterator<JsonNode> getListRecords(JsonNode result, String targetJsonName)
350
- {
351
- if (!result.has(targetJsonName) || !result.get(targetJsonName).isArray()) {
352
- throw new DataException(String.format("Missing '%s' from Zendesk API response", targetJsonName));
353
- }
354
- return result.get(targetJsonName).elements();
355
- }
356
-
357
241
  private JsonNode buildColumns(final PluginTask task)
358
242
  {
359
- JsonNode jsonNode = getZendeskSupportAPIService(task).getData("", 0, true, 0);
243
+ JsonNode jsonNode = getZendeskService(task).getDataFromPath("", 0, true, 0);
360
244
 
361
245
  String targetName = task.getTarget().getJsonName();
362
246
 
363
- if (jsonNode.has(targetName) && jsonNode.get(targetName).isArray() && jsonNode.get(targetName).size() > 0) {
247
+ if (jsonNode.has(targetName) && !jsonNode.get(targetName).isNull() && jsonNode.get(targetName).isArray() && jsonNode.get(targetName).size() > 0) {
364
248
  return addAllColumnsToSchema(jsonNode, task.getTarget(), task.getIncludes());
365
249
  }
366
250
  throw new ConfigException("Could not guess schema due to empty data set");
367
251
  }
368
252
 
369
253
  private final Pattern idPattern = Pattern.compile(ZendeskConstants.Regex.ID);
254
+
370
255
  private JsonNode addAllColumnsToSchema(final JsonNode jsonNode, final Target target, final List<String> includes)
371
256
  {
372
257
  final JsonNode sample = new ObjectMapper().valueToTree(StreamSupport.stream(
@@ -394,6 +279,11 @@ public class ZendeskInputPlugin implements InputPlugin
394
279
  }
395
280
  entry.put("type", Types.LONG.getName());
396
281
  }
282
+
283
+ // Id of User Events target is more suitable for String
284
+ if (target.equals(Target.USER_EVENTS)) {
285
+ entry.put("type", Types.STRING.getName());
286
+ }
397
287
  }
398
288
  else if (idPattern.matcher(name).find()) {
399
289
  if (type.equals(Types.TIMESTAMP.getName())) {
@@ -417,60 +307,150 @@ public class ZendeskInputPlugin implements InputPlugin
417
307
  .forEach(arrayNode::add);
418
308
  }
419
309
 
420
- private void fetchData(final JsonNode jsonNode, final PluginTask task, final Schema schema,
421
- final PageBuilder pageBuilder)
310
+ private ConfigSource createGuessConfig()
311
+ {
312
+ return Exec.newConfigSource()
313
+ .set("guess_plugins", ImmutableList.of("zendesk"))
314
+ .set("guess_sample_buffer_bytes", ZendeskConstants.Misc.GUESS_BUFFER_SIZE);
315
+ }
316
+
317
+ private ZendeskService getZendeskService(PluginTask task)
318
+ {
319
+ if (zendeskService == null) {
320
+ zendeskService = dispatchPerTarget(task);
321
+ }
322
+ return zendeskService;
323
+ }
324
+
325
+ @VisibleForTesting
326
+ protected ZendeskService dispatchPerTarget(ZendeskInputPlugin.PluginTask task)
327
+ {
328
+ switch (task.getTarget()) {
329
+ case TICKETS:
330
+ case USERS:
331
+ case ORGANIZATIONS:
332
+ case TICKET_METRICS:
333
+ case TICKET_EVENTS:
334
+ case TICKET_FORMS:
335
+ case TICKET_FIELDS:
336
+ return new ZendeskSupportAPIService(task);
337
+ case RECIPIENTS:
338
+ case SCORES:
339
+ return new ZendeskNPSService(task);
340
+ case OBJECT_RECORDS:
341
+ case RELATIONSHIP_RECORDS:
342
+ return new ZendeskCustomObjectService(task);
343
+ case USER_EVENTS:
344
+ return new ZendeskUserEventService(task);
345
+ default:
346
+ throw new ConfigException("Unsupported " + task.getTarget() + ", supported values: '" + Arrays.toString(Target.values()) + "'");
347
+ }
348
+ }
349
+
350
+ private RecordImporter getRecordImporter(Schema schema, PageBuilder pageBuilder)
351
+ {
352
+ if (recordImporter == null) {
353
+ recordImporter = new RecordImporter(schema, pageBuilder);
354
+ }
355
+ return recordImporter;
356
+ }
357
+
358
+ private void validateInputTask(PluginTask task)
359
+ {
360
+ validateAppMarketPlace(task.getAppMarketPlaceIntegrationName().isPresent(),
361
+ task.getAppMarketPlaceAppId().isPresent(),
362
+ task.getAppMarketPlaceOrgId().isPresent());
363
+ validateCredentials(task);
364
+ validateIncremental(task);
365
+ validateCustomObject(task);
366
+ validateUserEvent(task);
367
+ }
368
+
369
+ private void validateCredentials(PluginTask task)
422
370
  {
423
- // FIXME: if include is not contained in schema, data should be ignore
424
- task.getIncludes().forEach(include -> {
425
- String relatedObjectName = include.trim();
426
- final String url = task.getLoginUrl()
427
- + "/"
428
- + ZendeskConstants.Url.API
429
- + "/" + task.getTarget().toString()
430
- + "/" + jsonNode.get(ZendeskConstants.Field.ID).asText()
431
- + "/" + relatedObjectName + ".json";
432
-
433
- try {
434
- final JsonNode result = getZendeskSupportAPIService(task).getData(url, 0, false, 0);
435
- if (result != null && result.has(relatedObjectName)) {
436
- ((ObjectNode) jsonNode).set(include, result.get(relatedObjectName));
371
+ switch (task.getAuthenticationMethod()) {
372
+ case OAUTH:
373
+ if (!task.getAccessToken().isPresent()) {
374
+ throw new ConfigException(String.format("access_token is required for authentication method '%s'",
375
+ task.getAuthenticationMethod().name().toLowerCase()));
437
376
  }
438
- }
439
- catch (final ConfigException e) {
440
- // Sometimes we get 404 when having invalid endpoint, so ignore when we get 404 InvalidEndpoint
441
- if (!(e.getCause() instanceof ZendeskException && ((ZendeskException) e.getCause()).getStatusCode() == HttpStatus.SC_NOT_FOUND)) {
442
- throw e;
377
+ break;
378
+ case TOKEN:
379
+ if (!task.getUsername().isPresent() || !task.getToken().isPresent()) {
380
+ throw new ConfigException(String.format("username and token are required for authentication method '%s'",
381
+ task.getAuthenticationMethod().name().toLowerCase()));
443
382
  }
444
- }
445
- });
383
+ break;
384
+ case BASIC:
385
+ if (!task.getUsername().isPresent() || !task.getPassword().isPresent()) {
386
+ throw new ConfigException(String.format("username and password are required for authentication method '%s'",
387
+ task.getAuthenticationMethod().name().toLowerCase()));
388
+ }
389
+ break;
390
+ default:
391
+ throw new ConfigException("Unknown authentication method");
392
+ }
393
+ }
446
394
 
447
- ZendeskUtils.addRecord(jsonNode, schema, pageBuilder);
395
+ private void validateAppMarketPlace(final boolean isAppMarketIntegrationNamePresent,
396
+ final boolean isAppMarketAppIdPresent,
397
+ final boolean isAppMarketOrgIdPresent)
398
+ {
399
+ final boolean isAllAvailable =
400
+ isAppMarketIntegrationNamePresent && isAppMarketAppIdPresent && isAppMarketOrgIdPresent;
401
+ final boolean isAllUnAvailable =
402
+ !isAppMarketIntegrationNamePresent && !isAppMarketAppIdPresent && !isAppMarketOrgIdPresent;
403
+ // All or nothing needed
404
+ if (!(isAllAvailable || isAllUnAvailable)) {
405
+ throw new ConfigException("All of app_marketplace_integration_name, app_marketplace_org_id, " +
406
+ "app_marketplace_app_id " +
407
+ "are required to fill out for Apps Marketplace API header");
408
+ }
448
409
  }
449
410
 
450
- private ConfigSource createGuessConfig()
411
+ private void validateIncremental(PluginTask task)
451
412
  {
452
- return Exec.newConfigSource()
453
- .set("guess_plugins", ImmutableList.of("zendesk"))
454
- .set("guess_sample_buffer_bytes", ZendeskConstants.Misc.GUESS_BUFFER_SIZE);
413
+ if (task.getIncremental() && getZendeskService(task).isSupportIncremental()) {
414
+ if (!task.getDedup()) {
415
+ logger.warn("You've selected to skip de-duplicating records, result may contain duplicated data");
416
+ }
417
+
418
+ if (!getZendeskService(task).isSupportIncremental() && task.getStartTime().isPresent()) {
419
+ logger.warn(String.format("Target: '%s' doesn't support incremental export API. Will be ignored start_time option",
420
+ task.getTarget()));
421
+ }
422
+ }
455
423
  }
456
424
 
457
- private boolean isUpdatedBySystem(JsonNode recordJsonNode, long startTime)
425
+ private void validateCustomObject(PluginTask task)
458
426
  {
459
- /*
460
- * https://developer.zendesk.com/rest_api/docs/core/incremental_export#excluding-system-updates
461
- * "generated_timestamp" will be updated when Zendesk internal changing
462
- * "updated_at" will be updated when ticket data was changed
463
- * start_time for query parameter will be processed on Zendesk with generated_timestamp,
464
- * but it was calculated by record' updated_at time.
465
- * So the doesn't changed record from previous import would be appear by Zendesk internal changes.
466
- * We ignore record that has updated_at <= start_time
467
- */
468
- if (recordJsonNode.has(ZendeskConstants.Field.GENERATED_TIMESTAMP) && recordJsonNode.has(ZendeskConstants.Field.UPDATED_AT)) {
469
- String recordUpdatedAtTime = recordJsonNode.get(ZendeskConstants.Field.UPDATED_AT).asText();
470
- long recordUpdatedAtToEpochSecond = ZendeskDateUtils.isoToEpochSecond(recordUpdatedAtTime);
471
- return recordUpdatedAtToEpochSecond <= startTime;
427
+ if (task.getTarget().equals(Target.OBJECT_RECORDS) && task.getObjectTypes().isEmpty()) {
428
+ throw new ConfigException("Should have at least one Object Type");
429
+ }
430
+
431
+ if (task.getTarget().equals(Target.RELATIONSHIP_RECORDS) && task.getRelationshipTypes().isEmpty()) {
432
+ throw new ConfigException("Should have at least one Relationship Type");
472
433
  }
434
+ }
473
435
 
474
- return false;
436
+ private void validateUserEvent(PluginTask task)
437
+ {
438
+ if (task.getTarget().equals(Target.USER_EVENTS)) {
439
+ if (!task.getProfileSource().isPresent()) {
440
+ throw new ConfigException("Profile Source is required for User Event Target");
441
+ }
442
+
443
+ // Can't set end_time to 0, so it should be valid
444
+ task.getEndTime().ifPresent(time -> {
445
+ if (!ZendeskDateUtils.supportedTimeFormat(task.getEndTime().get()).isPresent()) {
446
+ throw new ConfigException("End Time should follow these format " + ZendeskConstants.Misc.SUPPORT_DATE_TIME_FORMAT.toString());
447
+ }
448
+ });
449
+
450
+ if (task.getStartTime().isPresent() && task.getEndTime().isPresent()
451
+ && ZendeskDateUtils.getStartTime(task.getStartTime().get()) > ZendeskDateUtils.isoToEpochSecond(task.getEndTime().get())) {
452
+ throw new ConfigException("End Time should be later or equal than Start Time");
453
+ }
454
+ }
475
455
  }
476
456
  }