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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/README.md +32 -2
- data/build.gradle +1 -1
- data/src/main/java/org/embulk/input/zendesk/RecordImporter.java +134 -0
- data/src/main/java/org/embulk/input/zendesk/ZendeskInputPlugin.java +182 -202
- data/src/main/java/org/embulk/input/zendesk/clients/ZendeskRestClient.java +54 -52
- data/src/main/java/org/embulk/input/zendesk/models/Target.java +3 -3
- data/src/main/java/org/embulk/input/zendesk/models/ZendeskException.java +1 -1
- data/src/main/java/org/embulk/input/zendesk/services/ZendeskCustomObjectService.java +110 -0
- data/src/main/java/org/embulk/input/zendesk/services/ZendeskNPSService.java +30 -0
- data/src/main/java/org/embulk/input/zendesk/services/ZendeskNormalServices.java +239 -0
- data/src/main/java/org/embulk/input/zendesk/services/ZendeskService.java +14 -0
- data/src/main/java/org/embulk/input/zendesk/services/ZendeskSupportAPIService.java +25 -83
- data/src/main/java/org/embulk/input/zendesk/services/ZendeskUserEventService.java +158 -0
- data/src/main/java/org/embulk/input/zendesk/stream/PagingSpliterator.java +40 -0
- data/src/main/java/org/embulk/input/zendesk/stream/paginator/sunshine/CustomObjectSpliterator.java +42 -0
- data/src/main/java/org/embulk/input/zendesk/stream/paginator/sunshine/SunshineSpliterator.java +66 -0
- data/src/main/java/org/embulk/input/zendesk/stream/paginator/sunshine/UserEventSpliterator.java +35 -0
- data/src/main/java/org/embulk/input/zendesk/stream/paginator/support/OrganizationSpliterator.java +13 -0
- data/src/main/java/org/embulk/input/zendesk/stream/paginator/support/SupportSpliterator.java +44 -0
- data/src/main/java/org/embulk/input/zendesk/stream/paginator/support/UserSpliterator.java +13 -0
- data/src/main/java/org/embulk/input/zendesk/utils/ZendeskConstants.java +13 -1
- data/src/main/java/org/embulk/input/zendesk/utils/ZendeskDateUtils.java +22 -11
- data/src/main/java/org/embulk/input/zendesk/utils/ZendeskUtils.java +52 -114
- data/src/test/java/org/embulk/input/zendesk/TestRecordImporter.java +114 -0
- data/src/test/java/org/embulk/input/zendesk/TestZendeskInputPlugin.java +184 -99
- data/src/test/java/org/embulk/input/zendesk/clients/TestZendeskRestClient.java +6 -20
- data/src/test/java/org/embulk/input/zendesk/services/TestZendeskCustomObjectService.java +161 -0
- data/src/test/java/org/embulk/input/zendesk/services/TestZendeskNPSService.java +56 -0
- data/src/test/java/org/embulk/input/zendesk/services/TestZendeskNormalService.java +189 -0
- data/src/test/java/org/embulk/input/zendesk/services/TestZendeskSupportAPIService.java +18 -60
- data/src/test/java/org/embulk/input/zendesk/services/TestZendeskUserEventService.java +158 -0
- data/src/test/java/org/embulk/input/zendesk/utils/TestZendeskDateUtils.java +50 -2
- data/src/test/java/org/embulk/input/zendesk/utils/TestZendeskUtil.java +0 -138
- data/src/test/java/org/embulk/input/zendesk/utils/ZendeskTestHelper.java +16 -0
- data/src/test/resources/config/nps.yml +29 -0
- data/src/test/resources/config/object_records.yml +24 -0
- data/src/test/resources/config/relationship_records.yml +23 -0
- data/src/test/resources/config/user_events.yml +29 -0
- data/src/test/resources/data/duplicate_user.json +0 -0
- data/src/test/resources/data/empty_result.json +7 -0
- data/src/test/resources/data/expected/user_events_column.json +40 -0
- data/src/test/resources/data/object_records.json +30 -0
- data/src/test/resources/data/organization.json +39 -0
- data/src/test/resources/data/relationship_records.json +57 -0
- data/src/test/resources/data/scores.json +21 -0
- data/src/test/resources/data/scores_share_same_time_with_next_page.json +35 -0
- data/src/test/resources/data/scores_share_same_time_without_next_page.json +35 -0
- data/src/test/resources/data/simple_organization.json +23 -0
- data/src/test/resources/data/simple_user.json +50 -0
- data/src/test/resources/data/simple_user_event.json +19 -0
- data/src/test/resources/data/ticket_events_share_same_time_with_next_page.json +279 -0
- data/src/test/resources/data/ticket_events_share_same_time_without_next_page.json +279 -0
- data/src/test/resources/data/ticket_events_updated_by_system_records.json +279 -0
- data/src/test/resources/data/ticket_share_same_time_with_next_page.json +232 -0
- data/src/test/resources/data/ticket_share_same_time_without_next_page.json +232 -0
- data/src/test/resources/data/ticket_with_updated_by_system_records.json +187 -0
- data/src/test/resources/data/user_event.json +19 -0
- data/src/test/resources/data/user_event_contain_latter_create_at.json +19 -0
- data/src/test/resources/data/user_event_multiple.json +33 -0
- metadata +46 -5
- data/src/main/java/org/embulk/input/zendesk/utils/ZendeskValidatorUtils.java +0 -79
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2dab4909dba68349742e03eef6be0ff02014caf1
|
|
4
|
+
data.tar.gz: 05bee8746fae83639eeb0daacb4b38ac5f765868
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0b0831ce1c1a40d1d9a3b2b3e8a73d8a2fa14a6e5144d264e1e210f2289c092fed6778c7ccea823fcf4464cc8b8ef61182306426730f4a82c85a50b262783bdf
|
|
7
|
+
data.tar.gz: e1706af5499ff07076761b0aa591920abf867a2a95d38c4b5e18a47dd61e73ecb653cf5f3d4cca8beb631ddf3f6b3de9018ebb92fecf8558a4f6e3713c4b0715
|
data/CHANGELOG.md
CHANGED
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 `
|
|
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
|
@@ -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.
|
|
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
|
|
159
|
+
private ZendeskService zendeskService;
|
|
160
|
+
|
|
161
|
+
private RecordImporter recordImporter;
|
|
141
162
|
|
|
142
|
-
private
|
|
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
|
-
|
|
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 (!
|
|
155
|
-
final JsonNode result =
|
|
156
|
-
if (result.has(ZendeskConstants.Field.COUNT) && result.get(ZendeskConstants.Field.COUNT).
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
421
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
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
|
|
411
|
+
private void validateIncremental(PluginTask task)
|
|
451
412
|
{
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
.
|
|
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
|
|
425
|
+
private void validateCustomObject(PluginTask task)
|
|
458
426
|
{
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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
|
}
|