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