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
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
  }