embulk-input-zendesk 0.2.14 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +9 -3
  3. data/.travis.yml +5 -44
  4. data/CHANGELOG.md +3 -0
  5. data/README.md +5 -5
  6. data/build.gradle +123 -0
  7. data/classpath/commons-codec-1.10.jar +0 -0
  8. data/classpath/commons-logging-1.2.jar +0 -0
  9. data/classpath/embulk-input-zendesk-0.3.0.jar +0 -0
  10. data/classpath/httpclient-4.5.6.jar +0 -0
  11. data/classpath/httpcore-4.4.10.jar +0 -0
  12. data/config/checkstyle/checkstyle.xml +128 -0
  13. data/config/checkstyle/default.xml +108 -0
  14. data/gradle/wrapper/gradle-wrapper.jar +0 -0
  15. data/gradle/wrapper/gradle-wrapper.properties +5 -0
  16. data/gradlew +172 -0
  17. data/gradlew.bat +84 -0
  18. data/lib/embulk/guess/zendesk.rb +21 -0
  19. data/lib/embulk/input/zendesk.rb +3 -9
  20. data/src/main/java/org/embulk/input/zendesk/ZendeskInputPlugin.java +471 -0
  21. data/src/main/java/org/embulk/input/zendesk/clients/ZendeskRestClient.java +268 -0
  22. data/src/main/java/org/embulk/input/zendesk/models/AuthenticationMethod.java +23 -0
  23. data/src/main/java/org/embulk/input/zendesk/models/Target.java +46 -0
  24. data/src/main/java/org/embulk/input/zendesk/models/ZendeskException.java +25 -0
  25. data/src/main/java/org/embulk/input/zendesk/services/ZendeskSupportAPIService.java +109 -0
  26. data/src/main/java/org/embulk/input/zendesk/utils/ZendeskConstants.java +61 -0
  27. data/src/main/java/org/embulk/input/zendesk/utils/ZendeskDateUtils.java +51 -0
  28. data/src/main/java/org/embulk/input/zendesk/utils/ZendeskUtils.java +150 -0
  29. data/src/main/java/org/embulk/input/zendesk/utils/ZendeskValidatorUtils.java +92 -0
  30. data/src/test/java/org/embulk/input/zendesk/TestZendeskInputPlugin.java +232 -0
  31. data/src/test/java/org/embulk/input/zendesk/clients/TestZendeskRestClient.java +351 -0
  32. data/src/test/java/org/embulk/input/zendesk/services/TestZendeskSupportAPIService.java +172 -0
  33. data/src/test/java/org/embulk/input/zendesk/utils/TestZendeskDateUtils.java +36 -0
  34. data/src/test/java/org/embulk/input/zendesk/utils/TestZendeskUtil.java +160 -0
  35. data/src/test/java/org/embulk/input/zendesk/utils/TestZendeskValidatorUtils.java +138 -0
  36. data/src/test/java/org/embulk/input/zendesk/utils/ZendeskPluginTestRuntime.java +133 -0
  37. data/src/test/java/org/embulk/input/zendesk/utils/ZendeskTestHelper.java +63 -0
  38. data/src/test/resources/config/base.yml +14 -0
  39. data/src/test/resources/config/base_validator.yml +48 -0
  40. data/src/test/resources/config/incremental.yml +54 -0
  41. data/src/test/resources/config/non-incremental.yml +39 -0
  42. data/src/test/resources/config/util.yml +18 -0
  43. data/src/test/resources/data/client.json +293 -0
  44. data/src/test/resources/data/error_data.json +187 -0
  45. data/src/test/resources/data/expected/ticket_column.json +148 -0
  46. data/src/test/resources/data/expected/ticket_column_with_related_objects.json +152 -0
  47. data/src/test/resources/data/expected/ticket_fields_column.json +92 -0
  48. data/src/test/resources/data/expected/ticket_metrics_column.json +98 -0
  49. data/src/test/resources/data/ticket_fields.json +225 -0
  50. data/src/test/resources/data/ticket_metrics.json +397 -0
  51. data/src/test/resources/data/ticket_with_related_objects.json +67 -0
  52. data/src/test/resources/data/tickets.json +232 -0
  53. data/src/test/resources/data/tickets_continue.json +52 -0
  54. data/src/test/resources/data/util.json +19 -0
  55. data/src/test/resources/data/util_page.json +227 -0
  56. metadata +65 -221
  57. data/.ruby-version +0 -1
  58. data/.travis.yml.erb +0 -43
  59. data/Gemfile +0 -2
  60. data/Rakefile +0 -21
  61. data/embulk-input-zendesk.gemspec +0 -29
  62. data/gemfiles/embulk-0.8.0-latest +0 -4
  63. data/gemfiles/embulk-0.8.1 +0 -4
  64. data/gemfiles/embulk-latest +0 -4
  65. data/gemfiles/template.erb +0 -4
  66. data/lib/embulk/input/zendesk/client.rb +0 -434
  67. data/lib/embulk/input/zendesk/plugin.rb +0 -199
  68. data/test/capture_io.rb +0 -45
  69. data/test/embulk/input/zendesk/test_client.rb +0 -722
  70. data/test/embulk/input/zendesk/test_plugin.rb +0 -628
  71. data/test/fixture_helper.rb +0 -11
  72. data/test/fixtures/invalid_app_marketplace_lack_one_property.yml +0 -13
  73. data/test/fixtures/invalid_app_marketplace_lack_two_property.yml +0 -12
  74. data/test/fixtures/invalid_lack_username.yml +0 -9
  75. data/test/fixtures/invalid_unknown_auth.yml +0 -9
  76. data/test/fixtures/tickets.json +0 -44
  77. data/test/fixtures/valid_app_marketplace.yml +0 -14
  78. data/test/fixtures/valid_auth_basic.yml +0 -11
  79. data/test/fixtures/valid_auth_oauth.yml +0 -10
  80. data/test/fixtures/valid_auth_token.yml +0 -11
  81. data/test/override_assert_raise.rb +0 -21
  82. data/test/run-test.rb +0 -26
Binary file
@@ -0,0 +1,5 @@
1
+ distributionBase=GRADLE_USER_HOME
2
+ distributionPath=wrapper/dists
3
+ zipStoreBase=GRADLE_USER_HOME
4
+ zipStorePath=wrapper/dists
5
+ distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-bin.zip
data/gradlew ADDED
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env sh
2
+
3
+ ##############################################################################
4
+ ##
5
+ ## Gradle start up script for UN*X
6
+ ##
7
+ ##############################################################################
8
+
9
+ # Attempt to set APP_HOME
10
+ # Resolve links: $0 may be a link
11
+ PRG="$0"
12
+ # Need this for relative symlinks.
13
+ while [ -h "$PRG" ] ; do
14
+ ls=`ls -ld "$PRG"`
15
+ link=`expr "$ls" : '.*-> \(.*\)$'`
16
+ if expr "$link" : '/.*' > /dev/null; then
17
+ PRG="$link"
18
+ else
19
+ PRG=`dirname "$PRG"`"/$link"
20
+ fi
21
+ done
22
+ SAVED="`pwd`"
23
+ cd "`dirname \"$PRG\"`/" >/dev/null
24
+ APP_HOME="`pwd -P`"
25
+ cd "$SAVED" >/dev/null
26
+
27
+ APP_NAME="Gradle"
28
+ APP_BASE_NAME=`basename "$0"`
29
+
30
+ # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31
+ DEFAULT_JVM_OPTS=""
32
+
33
+ # Use the maximum available, or set MAX_FD != -1 to use that value.
34
+ MAX_FD="maximum"
35
+
36
+ warn () {
37
+ echo "$*"
38
+ }
39
+
40
+ die () {
41
+ echo
42
+ echo "$*"
43
+ echo
44
+ exit 1
45
+ }
46
+
47
+ # OS specific support (must be 'true' or 'false').
48
+ cygwin=false
49
+ msys=false
50
+ darwin=false
51
+ nonstop=false
52
+ case "`uname`" in
53
+ CYGWIN* )
54
+ cygwin=true
55
+ ;;
56
+ Darwin* )
57
+ darwin=true
58
+ ;;
59
+ MINGW* )
60
+ msys=true
61
+ ;;
62
+ NONSTOP* )
63
+ nonstop=true
64
+ ;;
65
+ esac
66
+
67
+ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68
+
69
+ # Determine the Java command to use to start the JVM.
70
+ if [ -n "$JAVA_HOME" ] ; then
71
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72
+ # IBM's JDK on AIX uses strange locations for the executables
73
+ JAVACMD="$JAVA_HOME/jre/sh/java"
74
+ else
75
+ JAVACMD="$JAVA_HOME/bin/java"
76
+ fi
77
+ if [ ! -x "$JAVACMD" ] ; then
78
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79
+
80
+ Please set the JAVA_HOME variable in your environment to match the
81
+ location of your Java installation."
82
+ fi
83
+ else
84
+ JAVACMD="java"
85
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86
+
87
+ Please set the JAVA_HOME variable in your environment to match the
88
+ location of your Java installation."
89
+ fi
90
+
91
+ # Increase the maximum file descriptors if we can.
92
+ if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93
+ MAX_FD_LIMIT=`ulimit -H -n`
94
+ if [ $? -eq 0 ] ; then
95
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96
+ MAX_FD="$MAX_FD_LIMIT"
97
+ fi
98
+ ulimit -n $MAX_FD
99
+ if [ $? -ne 0 ] ; then
100
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
101
+ fi
102
+ else
103
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104
+ fi
105
+ fi
106
+
107
+ # For Darwin, add options to specify how the application appears in the dock
108
+ if $darwin; then
109
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110
+ fi
111
+
112
+ # For Cygwin, switch paths to Windows format before running java
113
+ if $cygwin ; then
114
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116
+ JAVACMD=`cygpath --unix "$JAVACMD"`
117
+
118
+ # We build the pattern for arguments to be converted via cygpath
119
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120
+ SEP=""
121
+ for dir in $ROOTDIRSRAW ; do
122
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
123
+ SEP="|"
124
+ done
125
+ OURCYGPATTERN="(^($ROOTDIRS))"
126
+ # Add a user-defined pattern to the cygpath arguments
127
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129
+ fi
130
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
131
+ i=0
132
+ for arg in "$@" ; do
133
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135
+
136
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138
+ else
139
+ eval `echo args$i`="\"$arg\""
140
+ fi
141
+ i=$((i+1))
142
+ done
143
+ case $i in
144
+ (0) set -- ;;
145
+ (1) set -- "$args0" ;;
146
+ (2) set -- "$args0" "$args1" ;;
147
+ (3) set -- "$args0" "$args1" "$args2" ;;
148
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154
+ esac
155
+ fi
156
+
157
+ # Escape application args
158
+ save () {
159
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160
+ echo " "
161
+ }
162
+ APP_ARGS=$(save "$@")
163
+
164
+ # Collect all arguments for the java command, following the shell quoting and substitution rules
165
+ eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166
+
167
+ # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168
+ if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169
+ cd "$(dirname "$0")"
170
+ fi
171
+
172
+ exec "$JAVACMD" "$@"
data/gradlew.bat ADDED
@@ -0,0 +1,84 @@
1
+ @if "%DEBUG%" == "" @echo off
2
+ @rem ##########################################################################
3
+ @rem
4
+ @rem Gradle startup script for Windows
5
+ @rem
6
+ @rem ##########################################################################
7
+
8
+ @rem Set local scope for the variables with windows NT shell
9
+ if "%OS%"=="Windows_NT" setlocal
10
+
11
+ set DIRNAME=%~dp0
12
+ if "%DIRNAME%" == "" set DIRNAME=.
13
+ set APP_BASE_NAME=%~n0
14
+ set APP_HOME=%DIRNAME%
15
+
16
+ @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17
+ set DEFAULT_JVM_OPTS=
18
+
19
+ @rem Find java.exe
20
+ if defined JAVA_HOME goto findJavaFromJavaHome
21
+
22
+ set JAVA_EXE=java.exe
23
+ %JAVA_EXE% -version >NUL 2>&1
24
+ if "%ERRORLEVEL%" == "0" goto init
25
+
26
+ echo.
27
+ echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28
+ echo.
29
+ echo Please set the JAVA_HOME variable in your environment to match the
30
+ echo location of your Java installation.
31
+
32
+ goto fail
33
+
34
+ :findJavaFromJavaHome
35
+ set JAVA_HOME=%JAVA_HOME:"=%
36
+ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37
+
38
+ if exist "%JAVA_EXE%" goto init
39
+
40
+ echo.
41
+ echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42
+ echo.
43
+ echo Please set the JAVA_HOME variable in your environment to match the
44
+ echo location of your Java installation.
45
+
46
+ goto fail
47
+
48
+ :init
49
+ @rem Get command-line arguments, handling Windows variants
50
+
51
+ if not "%OS%" == "Windows_NT" goto win9xME_args
52
+
53
+ :win9xME_args
54
+ @rem Slurp the command line arguments.
55
+ set CMD_LINE_ARGS=
56
+ set _SKIP=2
57
+
58
+ :win9xME_args_slurp
59
+ if "x%~1" == "x" goto execute
60
+
61
+ set CMD_LINE_ARGS=%*
62
+
63
+ :execute
64
+ @rem Setup the command line
65
+
66
+ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67
+
68
+ @rem Execute Gradle
69
+ "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70
+
71
+ :end
72
+ @rem End local scope for the variables with windows NT shell
73
+ if "%ERRORLEVEL%"=="0" goto mainEnd
74
+
75
+ :fail
76
+ rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77
+ rem the _cmd.exe /c_ return code!
78
+ if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79
+ exit /b 1
80
+
81
+ :mainEnd
82
+ if "%OS%"=="Windows_NT" endlocal
83
+
84
+ :omega
@@ -0,0 +1,21 @@
1
+ require 'json'
2
+
3
+ module Embulk
4
+ module Guess
5
+ class ZendeskGuess < TextGuessPlugin
6
+ Plugin.register_guess("zendesk", self)
7
+
8
+ def guess_text(config, sample_text)
9
+ {:columns =>
10
+ SchemaGuess.from_hash_records(JSON.parse(sample_text)).map do |c|
11
+ {
12
+ name: c.name,
13
+ type: c.type,
14
+ **(c.format ? {format: c.format} : {})
15
+ }
16
+ end
17
+ }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,9 +1,3 @@
1
- require "embulk/input/zendesk/client"
2
- require "embulk/input/zendesk/plugin"
3
-
4
- module Embulk
5
- module Input
6
- module Zendesk
7
- end
8
- end
9
- end
1
+ Embulk::JavaPlugin.register_input(
2
+ "zendesk", "org.embulk.input.zendesk.ZendeskInputPlugin",
3
+ File.expand_path('../../../../classpath', __FILE__))
@@ -0,0 +1,471 @@
1
+ package org.embulk.input.zendesk;
2
+
3
+ import com.fasterxml.jackson.databind.JsonNode;
4
+ import com.fasterxml.jackson.databind.ObjectMapper;
5
+ import com.fasterxml.jackson.databind.node.ArrayNode;
6
+ import com.fasterxml.jackson.databind.node.ObjectNode;
7
+ import com.google.common.annotations.VisibleForTesting;
8
+ import com.google.common.base.Throwables;
9
+ import com.google.common.collect.ImmutableList;
10
+
11
+ import org.embulk.config.Config;
12
+ import org.embulk.config.ConfigDefault;
13
+ import org.embulk.config.ConfigDiff;
14
+ import org.embulk.config.ConfigException;
15
+ import org.embulk.config.ConfigSource;
16
+ import org.embulk.config.Task;
17
+ import org.embulk.config.TaskReport;
18
+ import org.embulk.config.TaskSource;
19
+ import org.embulk.exec.GuessExecutor;
20
+ import org.embulk.input.zendesk.models.AuthenticationMethod;
21
+ import org.embulk.input.zendesk.models.Target;
22
+ import org.embulk.input.zendesk.services.ZendeskSupportAPIService;
23
+ import org.embulk.input.zendesk.utils.ZendeskConstants;
24
+ import org.embulk.input.zendesk.utils.ZendeskDateUtils;
25
+ import org.embulk.input.zendesk.utils.ZendeskUtils;
26
+ import org.embulk.input.zendesk.utils.ZendeskValidatorUtils;
27
+ import org.embulk.spi.Buffer;
28
+ import org.embulk.spi.DataException;
29
+ import org.embulk.spi.Exec;
30
+ import org.embulk.spi.InputPlugin;
31
+ import org.embulk.spi.PageBuilder;
32
+ import org.embulk.spi.PageOutput;
33
+ import org.embulk.spi.Schema;
34
+ import org.embulk.spi.SchemaConfig;
35
+ import org.embulk.spi.type.Types;
36
+ import org.slf4j.Logger;
37
+
38
+ import javax.validation.constraints.Max;
39
+ import javax.validation.constraints.Min;
40
+
41
+ import java.nio.charset.StandardCharsets;
42
+ import java.time.Instant;
43
+ import java.time.OffsetDateTime;
44
+ import java.time.ZoneOffset;
45
+ import java.time.format.DateTimeFormatter;
46
+ import java.util.Iterator;
47
+ import java.util.List;
48
+ import java.util.Optional;
49
+ import java.util.Set;
50
+ import java.util.concurrent.ConcurrentHashMap;
51
+ import java.util.concurrent.LinkedBlockingQueue;
52
+ import java.util.concurrent.ThreadPoolExecutor;
53
+ import java.util.concurrent.TimeUnit;
54
+ import java.util.regex.Pattern;
55
+ import java.util.stream.Collectors;
56
+ import java.util.stream.StreamSupport;
57
+
58
+ public class ZendeskInputPlugin implements InputPlugin
59
+ {
60
+ public interface PluginTask extends Task
61
+ {
62
+ @Config("login_url")
63
+ String getLoginUrl();
64
+
65
+ @Config("auth_method")
66
+ @ConfigDefault("\"basic\"")
67
+ AuthenticationMethod getAuthenticationMethod();
68
+
69
+ @Config("target")
70
+ Target getTarget();
71
+
72
+ @Config("username")
73
+ @ConfigDefault("null")
74
+ Optional<String> getUsername();
75
+
76
+ @Config("password")
77
+ @ConfigDefault("null")
78
+ Optional<String> getPassword();
79
+
80
+ @Config("token")
81
+ @ConfigDefault("null")
82
+ Optional<String> getToken();
83
+
84
+ @Config("access_token")
85
+ @ConfigDefault("null")
86
+ Optional<String> getAccessToken();
87
+
88
+ @Config("start_time")
89
+ @ConfigDefault("null")
90
+ Optional<String> getStartTime();
91
+
92
+ @Min(1)
93
+ @Max(30)
94
+ @Config("retry_limit")
95
+ @ConfigDefault("5")
96
+ int getRetryLimit();
97
+
98
+ @Min(1)
99
+ @Max(3600)
100
+ @Config("retry_initial_wait_sec")
101
+ @ConfigDefault("4")
102
+ int getRetryInitialWaitSec();
103
+
104
+ @Min(30)
105
+ @Max(3600)
106
+ @Config("max_retry_wait_sec")
107
+ @ConfigDefault("60")
108
+ int getMaxRetryWaitSec();
109
+
110
+ @Config("incremental")
111
+ @ConfigDefault("true")
112
+ boolean getIncremental();
113
+
114
+ @Config("includes")
115
+ @ConfigDefault("[]")
116
+ List<String> getIncludes();
117
+
118
+ @Config("dedup")
119
+ @ConfigDefault("true")
120
+ boolean getDedup();
121
+
122
+ @Config("app_marketplace_integration_name")
123
+ @ConfigDefault("null")
124
+ Optional<String> getAppMarketPlaceIntegrationName();
125
+
126
+ @Config("app_marketplace_org_id")
127
+ @ConfigDefault("null")
128
+ Optional<String> getAppMarketPlaceOrgId();
129
+
130
+ @Config("app_marketplace_app_id")
131
+ @ConfigDefault("null")
132
+ Optional<String> getAppMarketPlaceAppId();
133
+
134
+ @Config("columns")
135
+ SchemaConfig getColumns();
136
+ }
137
+
138
+ private static final Logger logger = Exec.getLogger(ZendeskInputPlugin.class);
139
+
140
+ private ZendeskSupportAPIService zendeskSupportAPIService;
141
+
142
+ @Override
143
+ public ConfigDiff transaction(final ConfigSource config, final Control control)
144
+ {
145
+ final PluginTask task = config.loadConfig(PluginTask.class);
146
+ ZendeskValidatorUtils.validateInputTask(task, getZendeskSupportAPIService(task));
147
+ final Schema schema = task.getColumns().toSchema();
148
+ int taskCount = 1;
149
+
150
+ // For non-incremental target, we will split records based on number of pages. 100 records per page
151
+ // In preview, run with taskCount = 1
152
+ if (!ZendeskUtils.isSupportAPIIncremental(task.getTarget()) && !Exec.isPreview()) {
153
+ final JsonNode result = getZendeskSupportAPIService(task).getData("", 0, false, 0);
154
+ if (result.has(ZendeskConstants.Field.COUNT) && result.get(ZendeskConstants.Field.COUNT).isInt()) {
155
+ taskCount = ZendeskUtils.numberToSplitWithHintingInTask(result.get(ZendeskConstants.Field.COUNT).asInt());
156
+ }
157
+ }
158
+ return resume(task.dump(), schema, taskCount, control);
159
+ }
160
+
161
+ @Override
162
+ public ConfigDiff resume(final TaskSource taskSource, final Schema schema, final int taskCount, final Control control)
163
+ {
164
+ final PluginTask task = taskSource.loadTask(PluginTask.class);
165
+ final List<TaskReport> taskReports = control.run(taskSource, schema, taskCount);
166
+ return this.buildConfigDiff(task, taskReports);
167
+ }
168
+
169
+ @Override
170
+ public void cleanup(final TaskSource taskSource, final Schema schema, final int taskCount, final List<TaskReport> successTaskReports)
171
+ {
172
+ }
173
+
174
+ @Override
175
+ public TaskReport run(final TaskSource taskSource, final Schema schema, final int taskIndex, final PageOutput output)
176
+ {
177
+ final PluginTask task = taskSource.loadTask(PluginTask.class);
178
+ try (final PageBuilder pageBuilder = getPageBuilder(schema, output)) {
179
+ final TaskReport taskReport = ingestServiceData(task, taskIndex, schema, pageBuilder);
180
+ pageBuilder.finish();
181
+ return taskReport;
182
+ }
183
+ }
184
+
185
+ @Override
186
+ public ConfigDiff guess(final ConfigSource config)
187
+ {
188
+ config.set("columns", new ObjectMapper().createArrayNode());
189
+ final PluginTask task = config.loadConfig(PluginTask.class);
190
+ ZendeskValidatorUtils.validateInputTask(task, getZendeskSupportAPIService(task));
191
+ return Exec.newConfigDiff().set("columns", buildColumns(task));
192
+ }
193
+
194
+ @VisibleForTesting
195
+ protected ZendeskSupportAPIService getZendeskSupportAPIService(final PluginTask task)
196
+ {
197
+ if (this.zendeskSupportAPIService == null) {
198
+ this.zendeskSupportAPIService = new ZendeskSupportAPIService(task);
199
+ }
200
+ this.zendeskSupportAPIService.setTask(task);
201
+ return this.zendeskSupportAPIService;
202
+ }
203
+
204
+ @VisibleForTesting
205
+ protected PageBuilder getPageBuilder(final Schema schema, final PageOutput output)
206
+ {
207
+ return new PageBuilder(Exec.getBufferAllocator(), schema, output);
208
+ }
209
+
210
+ private ConfigDiff buildConfigDiff(final PluginTask task, final List<TaskReport> taskReports)
211
+ {
212
+ final ConfigDiff configDiff = Exec.newConfigDiff();
213
+
214
+ if (!taskReports.isEmpty()) {
215
+ if (ZendeskUtils.isSupportAPIIncremental(task.getTarget())) {
216
+ final TaskReport taskReport = taskReports.get(0);
217
+ if (taskReport.has(ZendeskConstants.Field.START_TIME)) {
218
+ final OffsetDateTime offsetDateTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(
219
+ taskReport.get(JsonNode.class, ZendeskConstants.Field.START_TIME).asLong()),
220
+ ZoneOffset.UTC);
221
+
222
+ configDiff.set(ZendeskConstants.Field.START_TIME,
223
+ offsetDateTime.format(DateTimeFormatter.ofPattern(ZendeskConstants.Misc.RUBY_TIMESTAMP_FORMAT_INPUT)));
224
+ }
225
+ }
226
+ }
227
+ return configDiff;
228
+ }
229
+
230
+ private TaskReport ingestServiceData(final PluginTask task, final int taskIndex,
231
+ final Schema schema, final PageBuilder pageBuilder)
232
+ {
233
+ final TaskReport taskReport = Exec.newTaskReport();
234
+
235
+ if (ZendeskUtils.isSupportAPIIncremental(task.getTarget())) {
236
+ importDataForIncremental(task, schema, pageBuilder, taskReport);
237
+ }
238
+ else {
239
+ importDataForNonIncremental(task, taskIndex, schema, pageBuilder);
240
+ }
241
+
242
+ return taskReport;
243
+ }
244
+
245
+ private void importDataForIncremental(final PluginTask task, final Schema schema,
246
+ final PageBuilder pageBuilder, final TaskReport taskReport)
247
+ {
248
+ long startTime = 0;
249
+
250
+ if (ZendeskUtils.isSupportAPIIncremental(task.getTarget()) && task.getStartTime().isPresent()) {
251
+ startTime = ZendeskDateUtils.isoToEpochSecond(task.getStartTime().get());
252
+ }
253
+
254
+ // For incremental target, we will run in one task but split in multiple threads inside for data deduplication.
255
+ // Run with incremental will contain duplicated data.
256
+ ThreadPoolExecutor pool = null;
257
+ try {
258
+ Set<String> knownIds = ConcurrentHashMap.newKeySet();
259
+ pool = new ThreadPoolExecutor(
260
+ 10, 100, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>()
261
+ );
262
+
263
+ while (true) {
264
+ int recordCount = 0;
265
+
266
+ // Page argument isn't used in incremental API so we just set it to 0
267
+ final JsonNode result = getZendeskSupportAPIService(task).getData("", 0, false, startTime);
268
+ final Iterator<JsonNode> iterator = getListRecords(result, task.getTarget().getJsonName());
269
+
270
+ int numberOfRecords = 0;
271
+ if (result.has(ZendeskConstants.Field.COUNT)) {
272
+ numberOfRecords = result.get(ZendeskConstants.Field.COUNT).asInt();
273
+ }
274
+
275
+ while (iterator.hasNext()) {
276
+ final JsonNode recordJsonNode = iterator.next();
277
+
278
+ if (isUpdatedBySystem(recordJsonNode, startTime)) {
279
+ continue;
280
+ }
281
+
282
+ if (task.getDedup()) {
283
+ String recordID = recordJsonNode.get(ZendeskConstants.Field.ID).asText();
284
+ if (knownIds.contains(recordID)) {
285
+ continue;
286
+ }
287
+ knownIds.add(recordID);
288
+ }
289
+
290
+ pool.submit(() -> fetchData(recordJsonNode, task, schema, pageBuilder));
291
+ recordCount++;
292
+ if (Exec.isPreview()) {
293
+ return;
294
+ }
295
+ }
296
+ logger.info("Fetched '{}' records from start_time '{}'", recordCount, startTime);
297
+ if (result.has(ZendeskConstants.Field.END_TIME) && !result.get(ZendeskConstants.Field.END_TIME).isNull()
298
+ && result.has(task.getTarget().getJsonName())) {
299
+ // NOTE: start_time compared as "=>", not ">".
300
+ // If we will use end_time for next start_time, we got the same record that is last fetched
301
+ // end_time + 1 is workaround for that
302
+ taskReport.set("start_time", result.get(ZendeskConstants.Field.END_TIME).asLong() + 1);
303
+ }
304
+ else {
305
+ // Sometimes no record and no end_time fetched on the job, but we should generate start_time on config_diff.
306
+ taskReport.set("start_time", Instant.now().getEpochSecond());
307
+ }
308
+
309
+ if (numberOfRecords < ZendeskConstants.Misc.MAXIMUM_RECORDS_INCREMENTAL) {
310
+ break;
311
+ }
312
+ else {
313
+ startTime = result.get(ZendeskConstants.Field.END_TIME).asLong();
314
+ }
315
+ }
316
+ }
317
+ finally {
318
+ if (pool != null) {
319
+ pool.shutdown();
320
+ try {
321
+ pool.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
322
+ }
323
+ catch (final InterruptedException e) {
324
+ logger.warn("Error when wait pool to finish");
325
+ throw Throwables.propagate(e);
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+ private void importDataForNonIncremental(final PluginTask task, final int taskIndex, final Schema schema,
332
+ final PageBuilder pageBuilder)
333
+ {
334
+ // Page start from 1 => page = taskIndex + 1
335
+ final JsonNode result = getZendeskSupportAPIService(task).getData("", taskIndex + 1, false, 0);
336
+ final Iterator<JsonNode> iterator = getListRecords(result, task.getTarget().getJsonName());
337
+
338
+ while (iterator.hasNext()) {
339
+ fetchData(iterator.next(), task, schema, pageBuilder);
340
+
341
+ if (Exec.isPreview()) {
342
+ break;
343
+ }
344
+ }
345
+ }
346
+
347
+ private Iterator<JsonNode> getListRecords(JsonNode result, String targetJsonName)
348
+ {
349
+ if (!result.has(targetJsonName) || !result.get(targetJsonName).isArray()) {
350
+ throw new DataException(String.format("Missing '%s' from Zendesk API response", targetJsonName));
351
+ }
352
+ return result.get(targetJsonName).elements();
353
+ }
354
+
355
+ private JsonNode buildColumns(final PluginTask task)
356
+ {
357
+ JsonNode jsonNode = getZendeskSupportAPIService(task).getData("", 0, true, 0);
358
+
359
+ String targetName = task.getTarget().getJsonName();
360
+
361
+ if (jsonNode.has(targetName) && jsonNode.get(targetName).isArray() && jsonNode.get(targetName).size() > 0) {
362
+ return addAllColumnsToSchema(jsonNode, task.getTarget(), task.getIncludes());
363
+ }
364
+ throw new ConfigException("Could not guess schema due to empty data set");
365
+ }
366
+
367
+ private final Pattern idPattern = Pattern.compile(ZendeskConstants.Regex.ID);
368
+ private JsonNode addAllColumnsToSchema(final JsonNode jsonNode, final Target target, final List<String> includes)
369
+ {
370
+ final JsonNode sample = new ObjectMapper().valueToTree(StreamSupport.stream(
371
+ jsonNode.get(target.getJsonName()).spliterator(), false).limit(10).collect(Collectors.toList()));
372
+ final Buffer bufferSample = Buffer.copyOf(sample.toString().getBytes(StandardCharsets.UTF_8));
373
+ final JsonNode columns = Exec.getInjector().getInstance(GuessExecutor.class)
374
+ .guessParserConfig(bufferSample, Exec.newConfigSource(), createGuessConfig())
375
+ .getObjectNode().get("columns");
376
+
377
+ final Iterator<JsonNode> ite = columns.elements();
378
+
379
+ while (ite.hasNext()) {
380
+ final ObjectNode entry = (ObjectNode) ite.next();
381
+ final String name = entry.get("name").asText();
382
+ final String type = entry.get("type").asText();
383
+
384
+ if (type.equals(Types.TIMESTAMP.getName())) {
385
+ entry.put("format", ZendeskConstants.Misc.RUBY_TIMESTAMP_FORMAT);
386
+ }
387
+
388
+ if (name.equals("id")) {
389
+ if (!type.equals(Types.LONG.getName())) {
390
+ if (type.equals(Types.TIMESTAMP.getName())) {
391
+ entry.remove("format");
392
+ }
393
+ entry.put("type", Types.LONG.getName());
394
+ }
395
+ }
396
+ else if (idPattern.matcher(name).find()) {
397
+ if (type.equals(Types.TIMESTAMP.getName())) {
398
+ entry.remove("format");
399
+ }
400
+ entry.put("type", Types.STRING.getName());
401
+ }
402
+ }
403
+ addIncludedObjectsToSchema((ArrayNode) columns, includes);
404
+ return columns;
405
+ }
406
+
407
+ private void addIncludedObjectsToSchema(final ArrayNode arrayNode, final List<String> includes)
408
+ {
409
+ final ObjectMapper mapper = new ObjectMapper();
410
+
411
+ includes.stream()
412
+ .map((include) -> mapper.createObjectNode()
413
+ .put("name", include)
414
+ .put("type", Types.JSON.getName()))
415
+ .forEach(arrayNode::add);
416
+ }
417
+
418
+ private void fetchData(final JsonNode jsonNode, final PluginTask task, final Schema schema,
419
+ final PageBuilder pageBuilder)
420
+ {
421
+ // FIXME: if include is not contained in schema, data should be ignore
422
+ task.getIncludes().forEach(include -> {
423
+ String relatedObjectName = include.trim();
424
+ final String url = task.getLoginUrl() + ZendeskConstants.Url.API
425
+ + "/" + task.getTarget().toString()
426
+ + "/" + jsonNode.get(ZendeskConstants.Field.ID).asText()
427
+ + "/" + relatedObjectName + ".json";
428
+
429
+ try {
430
+ final JsonNode result = getZendeskSupportAPIService(task).getData(url, 0, false, 0);
431
+ if (result != null && result.has(relatedObjectName)) {
432
+ ((ObjectNode) jsonNode).set(include, result.get(relatedObjectName));
433
+ }
434
+ }
435
+ catch (final ConfigException e) {
436
+ // Sometimes we get 404 when having invalid endpoint, so ignore when we get 404 InvalidEndpoint
437
+ if (!e.getMessage().contains(ZendeskConstants.Misc.INVALID_END_POINT_RESPONSE)) {
438
+ throw e;
439
+ }
440
+ }
441
+ });
442
+ ZendeskUtils.addRecord(jsonNode, schema, pageBuilder);
443
+ }
444
+
445
+ private ConfigSource createGuessConfig()
446
+ {
447
+ return Exec.newConfigSource()
448
+ .set("guess_plugins", ImmutableList.of("zendesk"))
449
+ .set("guess_sample_buffer_bytes", ZendeskConstants.Misc.GUESS_BUFFER_SIZE);
450
+ }
451
+
452
+ private boolean isUpdatedBySystem(JsonNode recordJsonNode, long startTime)
453
+ {
454
+ /*
455
+ * https://developer.zendesk.com/rest_api/docs/core/incremental_export#excluding-system-updates
456
+ * "generated_timestamp" will be updated when Zendesk internal changing
457
+ * "updated_at" will be updated when ticket data was changed
458
+ * start_time for query parameter will be processed on Zendesk with generated_timestamp,
459
+ * but it was calculated by record' updated_at time.
460
+ * So the doesn't changed record from previous import would be appear by Zendesk internal changes.
461
+ * We ignore record that has updated_at <= start_time
462
+ */
463
+ if (recordJsonNode.has(ZendeskConstants.Field.GENERATED_TIMESTAMP) && recordJsonNode.has(ZendeskConstants.Field.UPDATED_AT)) {
464
+ String recordUpdatedAtTime = recordJsonNode.get(ZendeskConstants.Field.UPDATED_AT).asText();
465
+ long recordUpdatedAtToEpochSecond = ZendeskDateUtils.isoToEpochSecond(recordUpdatedAtTime);
466
+ return recordUpdatedAtToEpochSecond <= startTime;
467
+ }
468
+
469
+ return false;
470
+ }
471
+ }