embulk-input-zendesk-all 0.3.7
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 +7 -0
- data/.gitignore +12 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +126 -0
- data/LICENSE.txt +21 -0
- data/README.md +91 -0
- data/build.gradle +123 -0
- data/config/checkstyle/checkstyle.xml +128 -0
- data/config/checkstyle/default.xml +108 -0
- data/gradle/wrapper/gradle-wrapper.jar +0 -0
- data/gradle/wrapper/gradle-wrapper.properties +5 -0
- data/gradlew +172 -0
- data/gradlew.bat +84 -0
- data/lib/embulk/guess/zendesk.rb +21 -0
- data/lib/embulk/input/zendesk.rb +3 -0
- data/src/main/java/org/embulk/input/zendesk/RecordImporter.java +134 -0
- data/src/main/java/org/embulk/input/zendesk/ZendeskInputPlugin.java +513 -0
- data/src/main/java/org/embulk/input/zendesk/clients/ZendeskRestClient.java +291 -0
- data/src/main/java/org/embulk/input/zendesk/models/AuthenticationMethod.java +23 -0
- data/src/main/java/org/embulk/input/zendesk/models/Target.java +47 -0
- data/src/main/java/org/embulk/input/zendesk/models/ZendeskException.java +25 -0
- 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 +347 -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 +63 -0
- 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 +72 -0
- data/src/main/java/org/embulk/input/zendesk/utils/ZendeskDateUtils.java +68 -0
- data/src/main/java/org/embulk/input/zendesk/utils/ZendeskUtils.java +92 -0
- data/src/test/java/org/embulk/input/zendesk/TestRecordImporter.java +114 -0
- data/src/test/java/org/embulk/input/zendesk/TestZendeskInputPlugin.java +402 -0
- data/src/test/java/org/embulk/input/zendesk/clients/TestZendeskRestClient.java +337 -0
- 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 +261 -0
- data/src/test/java/org/embulk/input/zendesk/services/TestZendeskSupportAPIService.java +130 -0
- data/src/test/java/org/embulk/input/zendesk/services/TestZendeskUserEventService.java +158 -0
- data/src/test/java/org/embulk/input/zendesk/utils/TestZendeskDateUtils.java +87 -0
- data/src/test/java/org/embulk/input/zendesk/utils/TestZendeskUtil.java +22 -0
- data/src/test/java/org/embulk/input/zendesk/utils/ZendeskPluginTestRuntime.java +133 -0
- data/src/test/java/org/embulk/input/zendesk/utils/ZendeskTestHelper.java +92 -0
- data/src/test/resources/config/base.yml +14 -0
- data/src/test/resources/config/base_validator.yml +48 -0
- data/src/test/resources/config/incremental.yml +54 -0
- data/src/test/resources/config/non-incremental.yml +39 -0
- data/src/test/resources/config/nps.yml +31 -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/config/util.yml +18 -0
- data/src/test/resources/data/client.json +293 -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/error_data.json +187 -0
- data/src/test/resources/data/expected/ticket_column.json +148 -0
- data/src/test/resources/data/expected/ticket_column_with_related_objects.json +152 -0
- data/src/test/resources/data/expected/ticket_fields_column.json +92 -0
- data/src/test/resources/data/expected/ticket_metrics_column.json +98 -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_fields.json +225 -0
- data/src/test/resources/data/ticket_metrics.json +397 -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_related_objects.json +67 -0
- data/src/test/resources/data/ticket_with_updated_by_system_records.json +187 -0
- data/src/test/resources/data/tickets.json +232 -0
- data/src/test/resources/data/tickets_continue.json +52 -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
- data/src/test/resources/data/util.json +19 -0
- data/src/test/resources/data/util_page.json +227 -0
- metadata +168 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
package org.embulk.input.zendesk.clients;
|
|
2
|
+
|
|
3
|
+
import com.fasterxml.jackson.databind.DeserializationFeature;
|
|
4
|
+
import com.fasterxml.jackson.databind.JsonNode;
|
|
5
|
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
6
|
+
import com.google.common.annotations.VisibleForTesting;
|
|
7
|
+
import com.google.common.collect.ImmutableMap;
|
|
8
|
+
import com.google.common.collect.ImmutableMap.Builder;
|
|
9
|
+
import com.google.common.util.concurrent.RateLimiter;
|
|
10
|
+
import com.google.common.util.concurrent.Uninterruptibles;
|
|
11
|
+
import org.apache.http.Header;
|
|
12
|
+
import org.apache.http.HttpResponse;
|
|
13
|
+
import org.apache.http.HttpStatus;
|
|
14
|
+
import org.apache.http.client.HttpClient;
|
|
15
|
+
import org.apache.http.client.config.RequestConfig;
|
|
16
|
+
import org.apache.http.client.methods.HttpGet;
|
|
17
|
+
import org.apache.http.client.methods.HttpRequestBase;
|
|
18
|
+
import org.apache.http.impl.client.HttpClientBuilder;
|
|
19
|
+
import org.apache.http.util.EntityUtils;
|
|
20
|
+
import org.embulk.config.ConfigException;
|
|
21
|
+
import org.embulk.input.zendesk.ZendeskInputPlugin.PluginTask;
|
|
22
|
+
import org.embulk.input.zendesk.models.ZendeskException;
|
|
23
|
+
import org.embulk.input.zendesk.utils.ZendeskConstants;
|
|
24
|
+
import org.embulk.input.zendesk.utils.ZendeskUtils;
|
|
25
|
+
import org.embulk.spi.DataException;
|
|
26
|
+
import org.embulk.spi.Exec;
|
|
27
|
+
import org.embulk.spi.util.RetryExecutor;
|
|
28
|
+
import org.slf4j.Logger;
|
|
29
|
+
|
|
30
|
+
import java.io.IOException;
|
|
31
|
+
import java.util.concurrent.TimeUnit;
|
|
32
|
+
|
|
33
|
+
import static org.apache.http.HttpHeaders.AUTHORIZATION;
|
|
34
|
+
import static org.apache.http.protocol.HTTP.CONTENT_TYPE;
|
|
35
|
+
import static org.embulk.spi.util.RetryExecutor.retryExecutor;
|
|
36
|
+
|
|
37
|
+
public class ZendeskRestClient
|
|
38
|
+
{
|
|
39
|
+
private static final int CONNECTION_TIME_OUT = 240000;
|
|
40
|
+
|
|
41
|
+
private static final Logger logger = Exec.getLogger(ZendeskRestClient.class);
|
|
42
|
+
private static final ObjectMapper objectMapper = new ObjectMapper();
|
|
43
|
+
private static RateLimiter rateLimiter;
|
|
44
|
+
|
|
45
|
+
static {
|
|
46
|
+
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public ZendeskRestClient()
|
|
50
|
+
{
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public String doGet(final String url, final PluginTask task, final boolean isPreview)
|
|
54
|
+
{
|
|
55
|
+
try {
|
|
56
|
+
return retryExecutor().withRetryLimit(task.getRetryLimit())
|
|
57
|
+
.withInitialRetryWait(task.getRetryInitialWaitSec() * 1000)
|
|
58
|
+
.withMaxRetryWait(task.getMaxRetryWaitSec() * 1000)
|
|
59
|
+
.runInterruptible(new RetryExecutor.Retryable<String>()
|
|
60
|
+
{
|
|
61
|
+
@Override
|
|
62
|
+
public String call()
|
|
63
|
+
throws Exception
|
|
64
|
+
{
|
|
65
|
+
return sendGetRequest(url, task);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Override
|
|
69
|
+
public boolean isRetryableException(final Exception exception)
|
|
70
|
+
{
|
|
71
|
+
if (exception instanceof ZendeskException) {
|
|
72
|
+
final int statusCode = ((ZendeskException) exception).getStatusCode();
|
|
73
|
+
return isResponseStatusToRetry(statusCode, exception.getMessage(), ((ZendeskException) exception).getRetryAfter(), isPreview);
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Override
|
|
79
|
+
public void onRetry(final Exception exception, final int retryCount, final int retryLimit, final int retryWait)
|
|
80
|
+
{
|
|
81
|
+
if (exception instanceof ZendeskException) {
|
|
82
|
+
final int retryAfter = ((ZendeskException) exception).getRetryAfter();
|
|
83
|
+
final String message;
|
|
84
|
+
if (retryAfter > 0 && retryAfter > (retryWait / 1000)) {
|
|
85
|
+
message = String
|
|
86
|
+
.format("Retrying '%d'/'%d' after '%d' seconds. HTTP status code: '%s'",
|
|
87
|
+
retryCount, retryLimit,
|
|
88
|
+
retryAfter,
|
|
89
|
+
((ZendeskException) exception).getStatusCode());
|
|
90
|
+
logger.warn(message);
|
|
91
|
+
Uninterruptibles.sleepUninterruptibly(retryAfter - (retryWait / 1000), TimeUnit.SECONDS);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
message = String
|
|
95
|
+
.format("Retrying '%d'/'%d' after '%d' seconds. HTTP status code: '%s'",
|
|
96
|
+
retryCount, retryLimit,
|
|
97
|
+
retryWait / 1000,
|
|
98
|
+
((ZendeskException) exception).getStatusCode());
|
|
99
|
+
logger.warn(message);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
final String message = String
|
|
104
|
+
.format("Retrying '%d'/'%d' after '%d' seconds. Message: '%s'",
|
|
105
|
+
retryCount, retryLimit,
|
|
106
|
+
retryWait / 1000,
|
|
107
|
+
exception.getMessage());
|
|
108
|
+
logger.warn(message, exception);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Override
|
|
113
|
+
public void onGiveup(final Exception firstException, final Exception lastException)
|
|
114
|
+
{
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (final RetryExecutor.RetryGiveupException | InterruptedException e) {
|
|
119
|
+
if (e instanceof RetryExecutor.RetryGiveupException && e.getCause() != null && e.getCause() instanceof ZendeskException) {
|
|
120
|
+
throw new ConfigException("Status: '" + ((ZendeskException) (e.getCause())).getStatusCode() + "', error message: '" + e.getCause().getMessage() + "'",
|
|
121
|
+
e.getCause());
|
|
122
|
+
}
|
|
123
|
+
throw new ConfigException(e);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@VisibleForTesting
|
|
128
|
+
protected HttpClient createHttpClient()
|
|
129
|
+
{
|
|
130
|
+
final RequestConfig config = RequestConfig.custom()
|
|
131
|
+
.setConnectTimeout(CONNECTION_TIME_OUT)
|
|
132
|
+
.setConnectionRequestTimeout(CONNECTION_TIME_OUT)
|
|
133
|
+
.build();
|
|
134
|
+
return HttpClientBuilder.create().setDefaultRequestConfig(config).build();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private String sendGetRequest(final String url, final PluginTask task)
|
|
138
|
+
throws ZendeskException
|
|
139
|
+
{
|
|
140
|
+
try {
|
|
141
|
+
final HttpClient client = createHttpClient();
|
|
142
|
+
final HttpRequestBase request = createGetRequest(url, task);
|
|
143
|
+
|
|
144
|
+
if (rateLimiter != null) {
|
|
145
|
+
rateLimiter.acquire();
|
|
146
|
+
}
|
|
147
|
+
logger.info(">>> {}{}", request.getURI().getPath(),
|
|
148
|
+
request.getURI().getQuery() != null ? "?" + request.getURI().getQuery() : "");
|
|
149
|
+
final HttpResponse response = client.execute(request);
|
|
150
|
+
if (rateLimiter == null) {
|
|
151
|
+
initRateLimiter(response);
|
|
152
|
+
}
|
|
153
|
+
final int statusCode = response.getStatusLine().getStatusCode();
|
|
154
|
+
if (statusCode != HttpStatus.SC_OK) {
|
|
155
|
+
if (statusCode == ZendeskConstants.HttpStatus.TOO_MANY_REQUEST || statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR
|
|
156
|
+
|| statusCode == HttpStatus.SC_SERVICE_UNAVAILABLE) {
|
|
157
|
+
final Header retryHeader = response.getFirstHeader("Retry-After");
|
|
158
|
+
if (retryHeader != null) {
|
|
159
|
+
throw new ZendeskException(statusCode, EntityUtils.toString(response.getEntity()), Integer.parseInt(retryHeader.getValue()));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
throw new ZendeskException(statusCode, EntityUtils.toString(response.getEntity()), 0);
|
|
163
|
+
}
|
|
164
|
+
return EntityUtils.toString(response.getEntity());
|
|
165
|
+
}
|
|
166
|
+
catch (final IOException ex) {
|
|
167
|
+
throw new ZendeskException(-1, ex.getMessage(), 0);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private boolean isResponseStatusToRetry(final int status, final String message, final int retryAfter, final boolean isPreview)
|
|
172
|
+
{
|
|
173
|
+
if (status == HttpStatus.SC_NOT_FOUND) {
|
|
174
|
+
ObjectMapper objectMapper = new ObjectMapper();
|
|
175
|
+
try {
|
|
176
|
+
JsonNode jsonNode = objectMapper.readTree(message);
|
|
177
|
+
if (jsonNode.has("error") && jsonNode.get("error").has("title") && jsonNode.get("error").get("title").asText().startsWith("No help desk at ")) {
|
|
178
|
+
throw new ConfigException("This address is not registered in Zendesk. Please check the login_url again");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (IOException e) {
|
|
182
|
+
// In case we can't parse the message, error should not be show here
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 404 would be returned e.g. ticket comments are empty (on fetchRelatedObjects method)
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (status == HttpStatus.SC_CONFLICT) {
|
|
190
|
+
logger.warn(String.format("'%s' temporally failure.", status));
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (status == HttpStatus.SC_UNPROCESSABLE_ENTITY) {
|
|
195
|
+
if (message.startsWith(ZendeskConstants.Misc.TOO_RECENT_START_TIME)) {
|
|
196
|
+
//That means "No records from start_time". We can recognize it same as 200.
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
throw new ConfigException("Status: '" + status + "', error message: '" + message + "'");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (status == ZendeskConstants.HttpStatus.TOO_MANY_REQUEST || status == HttpStatus.SC_INTERNAL_SERVER_ERROR
|
|
203
|
+
|| status == HttpStatus.SC_SERVICE_UNAVAILABLE) {
|
|
204
|
+
if (!isPreview) {
|
|
205
|
+
if (retryAfter > 0) {
|
|
206
|
+
logger.warn("Reached API limitation, wait for at least '{}' '{}'", retryAfter, TimeUnit.SECONDS.name());
|
|
207
|
+
}
|
|
208
|
+
else if (status != ZendeskConstants.HttpStatus.TOO_MANY_REQUEST) {
|
|
209
|
+
logger.warn(String.format("'%s' temporally failure.", status));
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
throw new DataException("Rate Limited. Waiting '" + retryAfter + "' seconds to re-run");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Won't retry for 4xx range errors except above. Almost they should be ConfigError e.g. 403 Forbidden
|
|
217
|
+
if (status / 100 == 4) {
|
|
218
|
+
if (status == HttpStatus.SC_UNAUTHORIZED) {
|
|
219
|
+
throw new ConfigException("Cannot authenticate due to invalid login credentials");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (status == HttpStatus.SC_FORBIDDEN) {
|
|
223
|
+
throw new ConfigException("You do not have access to this resource. Please contact the account owner of this help desk for further help.");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
throw new ConfigException("Status '" + status + "', message '" + message + "'");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
logger.warn("Server returns unknown status code '" + status + "' message '" + message + "'");
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private HttpRequestBase createGetRequest(final String url, final PluginTask task)
|
|
234
|
+
{
|
|
235
|
+
final HttpGet request = new HttpGet(url);
|
|
236
|
+
final ImmutableMap<String, String> headers = buildAuthHeader(task);
|
|
237
|
+
headers.forEach(request::setHeader);
|
|
238
|
+
return request;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private ImmutableMap<String, String> buildAuthHeader(final PluginTask task)
|
|
242
|
+
{
|
|
243
|
+
final Builder<String, String> builder = new Builder<>();
|
|
244
|
+
builder.put(AUTHORIZATION, buildCredential(task));
|
|
245
|
+
addCommonHeader(builder, task);
|
|
246
|
+
return builder.build();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private String buildCredential(final PluginTask task)
|
|
250
|
+
{
|
|
251
|
+
switch (task.getAuthenticationMethod()) {
|
|
252
|
+
case BASIC:
|
|
253
|
+
return "Basic " + ZendeskUtils.convertBase64(String.format("%s:%s", task.getUsername().get(), task.getPassword().get()));
|
|
254
|
+
case TOKEN:
|
|
255
|
+
return "Basic " + ZendeskUtils.convertBase64(String.format("%s/token:%s", task.getUsername().get(), task.getToken().get()));
|
|
256
|
+
case OAUTH:
|
|
257
|
+
return "Bearer " + task.getAccessToken().get();
|
|
258
|
+
}
|
|
259
|
+
return "";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private void addCommonHeader(final Builder<String, String> builder, final PluginTask task)
|
|
263
|
+
{
|
|
264
|
+
task.getAppMarketPlaceIntegrationName().ifPresent(s -> builder.put(ZendeskConstants.Header.ZENDESK_MARKETPLACE_NAME, s));
|
|
265
|
+
task.getAppMarketPlaceAppId().ifPresent(s -> builder.put(ZendeskConstants.Header.ZENDESK_MARKETPLACE_APP_ID, s));
|
|
266
|
+
task.getAppMarketPlaceOrgId().ifPresent(s -> builder.put(ZendeskConstants.Header.ZENDESK_MARKETPLACE_ORGANIZATION_ID, s));
|
|
267
|
+
|
|
268
|
+
builder.put(CONTENT_TYPE, ZendeskConstants.Header.APPLICATION_JSON);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private synchronized void initRateLimiter(final HttpResponse response)
|
|
272
|
+
{
|
|
273
|
+
if (response.containsHeader("x-rate-limit")) {
|
|
274
|
+
double permits = 0.0;
|
|
275
|
+
final String rateLimit = response.getFirstHeader("x-rate-limit").getValue();
|
|
276
|
+
try {
|
|
277
|
+
permits = Double.parseDouble(rateLimit);
|
|
278
|
+
}
|
|
279
|
+
catch (final NumberFormatException e) {
|
|
280
|
+
throw new DataException("Error when parse x-rate-limit: '" + response.getFirstHeader("x-rate-limit").getValue() + "'");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (permits > 0) {
|
|
284
|
+
permits = permits / 60;
|
|
285
|
+
logger.info("Permits per second " + permits);
|
|
286
|
+
|
|
287
|
+
rateLimiter = RateLimiter.create(permits);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package org.embulk.input.zendesk.models;
|
|
2
|
+
|
|
3
|
+
import com.fasterxml.jackson.annotation.JsonCreator;
|
|
4
|
+
import org.embulk.config.ConfigException;
|
|
5
|
+
|
|
6
|
+
import java.util.Arrays;
|
|
7
|
+
|
|
8
|
+
public enum AuthenticationMethod
|
|
9
|
+
{
|
|
10
|
+
BASIC, OAUTH, TOKEN;
|
|
11
|
+
|
|
12
|
+
@JsonCreator
|
|
13
|
+
public static AuthenticationMethod fromString(final String value)
|
|
14
|
+
{
|
|
15
|
+
try {
|
|
16
|
+
return AuthenticationMethod.valueOf(value.trim().toUpperCase());
|
|
17
|
+
}
|
|
18
|
+
catch (IllegalArgumentException e) {
|
|
19
|
+
throw new ConfigException("Unsupported Authentication mode '" + value + "', supported values: '"
|
|
20
|
+
+ Arrays.toString(Target.values()) + "'");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package org.embulk.input.zendesk.models;
|
|
2
|
+
|
|
3
|
+
import com.fasterxml.jackson.annotation.JsonCreator;
|
|
4
|
+
import org.embulk.config.ConfigException;
|
|
5
|
+
|
|
6
|
+
import java.util.Arrays;
|
|
7
|
+
|
|
8
|
+
public enum Target
|
|
9
|
+
{
|
|
10
|
+
/** For ticket_metrics - we fetch by using include metric_sets with ticket target
|
|
11
|
+
* so the jsonName is different comparing to the target name
|
|
12
|
+
*/
|
|
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
|
+
TICKET_METRIC_EVENTS("ticket_metric_events"), SATISFACTION_RATINGS("satisfaction_ratings"),
|
|
16
|
+
RECIPIENTS("recipients"), SCORES("responses"), OBJECT_RECORDS("data"), RELATIONSHIP_RECORDS("data"), USER_EVENTS("data");
|
|
17
|
+
|
|
18
|
+
String jsonName;
|
|
19
|
+
|
|
20
|
+
Target(String jsonName)
|
|
21
|
+
{
|
|
22
|
+
this.jsonName = jsonName;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@JsonCreator
|
|
26
|
+
public static Target fromString(final String value)
|
|
27
|
+
{
|
|
28
|
+
try {
|
|
29
|
+
return Target.valueOf(value.trim().toUpperCase());
|
|
30
|
+
}
|
|
31
|
+
catch (IllegalArgumentException e) {
|
|
32
|
+
throw new ConfigException("Unsupported target '" + value + "', supported values: '"
|
|
33
|
+
+ Arrays.toString(Target.values()) + "'");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Override
|
|
38
|
+
public String toString()
|
|
39
|
+
{
|
|
40
|
+
return this.name().trim().toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public String getJsonName()
|
|
44
|
+
{
|
|
45
|
+
return this.jsonName;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package org.embulk.input.zendesk.models;
|
|
2
|
+
|
|
3
|
+
public class ZendeskException extends Exception
|
|
4
|
+
{
|
|
5
|
+
private static final long serialVersionUID = -256731723520584046L;
|
|
6
|
+
private final int statusCode;
|
|
7
|
+
private final int retryAfter;
|
|
8
|
+
|
|
9
|
+
public ZendeskException(final int statusCode, final String message, final int retryAfter)
|
|
10
|
+
{
|
|
11
|
+
super(message);
|
|
12
|
+
this.statusCode = statusCode;
|
|
13
|
+
this.retryAfter = retryAfter;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public int getStatusCode()
|
|
17
|
+
{
|
|
18
|
+
return statusCode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public int getRetryAfter()
|
|
22
|
+
{
|
|
23
|
+
return retryAfter;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -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
|
+
}
|