embulk-input-jira 0.2.5 → 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +10 -5
  3. data/.travis.yml +4 -34
  4. data/CHANGELOG.md +4 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +5 -4
  7. data/build.gradle +116 -0
  8. data/config/checkstyle/checkstyle.xml +128 -0
  9. data/config/checkstyle/default.xml +108 -0
  10. data/gradle/wrapper/gradle-wrapper.jar +0 -0
  11. data/gradle/wrapper/gradle-wrapper.properties +5 -0
  12. data/gradlew +172 -0
  13. data/gradlew.bat +84 -0
  14. data/lib/embulk/guess/jira.rb +24 -0
  15. data/lib/embulk/input/jira.rb +3 -169
  16. data/src/main/java/org/embulk/input/jira/AuthenticateMethod.java +27 -0
  17. data/src/main/java/org/embulk/input/jira/Constant.java +17 -0
  18. data/src/main/java/org/embulk/input/jira/Issue.java +150 -0
  19. data/src/main/java/org/embulk/input/jira/JiraInputPlugin.java +226 -0
  20. data/src/main/java/org/embulk/input/jira/client/JiraClient.java +254 -0
  21. data/src/main/java/org/embulk/input/jira/util/JiraException.java +18 -0
  22. data/src/main/java/org/embulk/input/jira/util/JiraUtil.java +264 -0
  23. data/src/test/java/org/embulk/input/jira/IssueTest.java +278 -0
  24. data/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java +204 -0
  25. data/src/test/java/org/embulk/input/jira/JiraPluginTestRuntime.java +133 -0
  26. data/src/test/java/org/embulk/input/jira/TestHelpers.java +41 -0
  27. data/src/test/java/org/embulk/input/jira/client/JiraClientTest.java +222 -0
  28. data/src/test/java/org/embulk/input/jira/util/JiraUtilTest.java +318 -0
  29. data/src/test/resources/config.yml +13 -0
  30. data/src/test/resources/issue_flatten.json +129 -0
  31. data/src/test/resources/issue_flatten_expected.json +73 -0
  32. data/src/test/resources/issue_get.json +36 -0
  33. data/src/test/resources/issue_get_expected.json +62 -0
  34. data/src/test/resources/jira_client.json +81 -0
  35. data/src/test/resources/jira_input_plugin.json +114 -0
  36. data/src/test/resources/jira_util.json +26 -0
  37. metadata +55 -175
  38. data/Gemfile +0 -3
  39. data/LICENSE +0 -13
  40. data/Rakefile +0 -15
  41. data/embulk-input-jira.gemspec +0 -27
  42. data/gemfiles/embulk-0.8.0-latest +0 -4
  43. data/gemfiles/embulk-0.8.7 +0 -4
  44. data/gemfiles/embulk-0.8.8 +0 -4
  45. data/gemfiles/embulk-latest +0 -4
  46. data/gemfiles/template.erb +0 -4
  47. data/lib/embulk/input/jira_api.rb +0 -9
  48. data/lib/embulk/input/jira_api/client.rb +0 -144
  49. data/lib/embulk/input/jira_api/issue.rb +0 -133
  50. data/lib/embulk/input/jira_input_plugin_utils.rb +0 -58
  51. data/spec/embulk/input/jira-input-plugin-utils_spec.rb +0 -89
  52. data/spec/embulk/input/jira_api/client_spec.rb +0 -224
  53. data/spec/embulk/input/jira_api/issue_spec.rb +0 -394
  54. data/spec/embulk/input/jira_spec.rb +0 -322
  55. data/spec/embulk_spec.rb +0 -32
  56. data/spec/spec_helper.rb +0 -26
  57. data/spec/support/stdout_and_err_capture.rb +0 -45
@@ -0,0 +1,27 @@
1
+ package org.embulk.input.jira;
2
+
3
+ import com.fasterxml.jackson.annotation.JsonCreator;
4
+ import com.fasterxml.jackson.annotation.JsonValue;
5
+
6
+ import org.embulk.config.ConfigException;
7
+
8
+ public enum AuthenticateMethod {
9
+ BASIC;
10
+ @JsonValue
11
+ @Override
12
+ public String toString()
13
+ {
14
+ return this.name().toLowerCase();
15
+ }
16
+
17
+ @JsonCreator
18
+ public static AuthenticateMethod fromString(String value)
19
+ {
20
+ switch(value) {
21
+ case "basic":
22
+ return BASIC;
23
+ default:
24
+ throw new ConfigException(String.format("Unknown AuthenticateMethod value '%s'. Supported values is basic.", value));
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,17 @@
1
+ package org.embulk.input.jira;
2
+
3
+ public final class Constant
4
+ {
5
+ public static final int MAX_RESULTS = 50;
6
+ public static final int MIN_RESULTS = 1;
7
+ public static final int GUESS_RECORDS_COUNT = 50;
8
+ public static final int PREVIEW_RECORDS_COUNT = 10;
9
+ public static final int GUESS_BUFFER_SIZE = 5 * 1024 * 1024;
10
+
11
+ public static final String DEFAULT_TIMESTAMP_PATTERN = "%Y-%m-%dT%H:%M:%S.%L%z";
12
+
13
+ public static final String CREDENTIAL_URI_PATH = "rest/api/latest/myself";
14
+ public static final String SEARCH_URI_PATH = "rest/api/latest/search";
15
+
16
+ private Constant(){}
17
+ }
@@ -0,0 +1,150 @@
1
+ package org.embulk.input.jira;
2
+
3
+ import com.google.gson.JsonArray;
4
+ import com.google.gson.JsonElement;
5
+ import com.google.gson.JsonNull;
6
+ import com.google.gson.JsonObject;
7
+ import com.google.gson.JsonPrimitive;
8
+ import org.apache.commons.lang3.StringUtils;
9
+
10
+ import java.util.ArrayList;
11
+ import java.util.Arrays;
12
+ import java.util.HashMap;
13
+ import java.util.List;
14
+ import java.util.Map;
15
+ import java.util.Map.Entry;
16
+ import java.util.stream.StreamSupport;
17
+
18
+ public class Issue
19
+ {
20
+ private JsonObject flatten;
21
+ private JsonObject json;
22
+
23
+ public Issue(JsonObject original)
24
+ {
25
+ this.json = original;
26
+ }
27
+
28
+ public JsonElement getValue(String path)
29
+ {
30
+ List<String> keys = new ArrayList<>(Arrays.asList(path.split("\\.")));
31
+ return get(json, keys);
32
+ }
33
+
34
+ private JsonElement get(JsonElement json, List<String> keys)
35
+ {
36
+ if (json == null || json.isJsonNull()) {
37
+ return JsonNull.INSTANCE;
38
+ }
39
+ else if (keys.isEmpty() || (json.isJsonArray() && json.getAsJsonArray().size() == 0)) {
40
+ return json;
41
+ }
42
+ String key = keys.get(0);
43
+ keys.remove(0);
44
+ if (json.isJsonArray()) {
45
+ JsonArray arrays = new JsonArray();
46
+ for (JsonElement elem : json.getAsJsonArray()) {
47
+ if (elem.isJsonObject()) {
48
+ arrays.add(elem.getAsJsonObject().get(key));
49
+ }
50
+ else {
51
+ arrays.add(elem);
52
+ }
53
+ }
54
+ return get(arrays, keys);
55
+ }
56
+ else {
57
+ return get(json.getAsJsonObject().get(key), keys);
58
+ }
59
+ }
60
+
61
+ public synchronized JsonObject getFlatten()
62
+ {
63
+ if (flatten == null) {
64
+ flatten = new JsonObject();
65
+ manipulatingFlattenJson(json, "");
66
+ }
67
+ return flatten;
68
+ }
69
+
70
+ private void manipulatingFlattenJson(JsonElement in, String prefix)
71
+ {
72
+ if (in.isJsonObject()) {
73
+ JsonObject obj = in.getAsJsonObject();
74
+ // NOTE: If you want to flatten JSON completely, please remove this if and addHeuristicValue
75
+ if (StringUtils.countMatches(prefix, ".") > 1) {
76
+ addHeuristicValue(obj, prefix);
77
+ return;
78
+ }
79
+ if (obj.entrySet().isEmpty()) {
80
+ flatten.add(prefix, obj);
81
+ }
82
+ else {
83
+ for (Entry<String, JsonElement> entry : obj.entrySet()) {
84
+ String key = entry.getKey();
85
+ JsonElement value = entry.getValue();
86
+ manipulatingFlattenJson(value, appendPrefix(prefix, key));
87
+ }
88
+ }
89
+ }
90
+ else if (in.isJsonArray()) {
91
+ JsonArray arrayObj = in.getAsJsonArray();
92
+ boolean isAllJsonObject = arrayObj.size() > 0 && StreamSupport.stream(arrayObj.spliterator(), false).allMatch(JsonElement::isJsonObject);
93
+ if (isAllJsonObject) {
94
+ Map<String, Integer> occurents = new HashMap<>();
95
+ for (JsonElement element : arrayObj) {
96
+ JsonObject obj = element.getAsJsonObject();
97
+ for (Entry<String, JsonElement> entry : obj.entrySet()) {
98
+ String key = entry.getKey();
99
+ occurents.merge(key, 1, Integer::sum);
100
+ }
101
+ }
102
+ JsonObject newObj = new JsonObject();
103
+ for (String key : occurents.keySet()) {
104
+ newObj.add(key, new JsonArray());
105
+ for (JsonElement elem : arrayObj) {
106
+ newObj.get(key).getAsJsonArray().add(elem.getAsJsonObject().get(key));
107
+ }
108
+ }
109
+ manipulatingFlattenJson(newObj, prefix);
110
+ }
111
+ else {
112
+ flatten.add(prefix,
113
+ new JsonPrimitive("String value"));
114
+ }
115
+ }
116
+ else if (in.isJsonPrimitive()) {
117
+ flatten.add(prefix, in.getAsJsonPrimitive());
118
+ }
119
+ else {
120
+ flatten.add(prefix, JsonNull.INSTANCE);
121
+ }
122
+ }
123
+
124
+ private void addHeuristicValue(JsonObject json, String prefix)
125
+ {
126
+ List<String> keys = Arrays.asList("name", "key", "id");
127
+ List<String> heuristic = new ArrayList<>();
128
+ for (Entry<String, JsonElement> entry : json.entrySet()) {
129
+ String key = entry.getKey();
130
+ JsonElement value = entry.getValue();
131
+ if (keys.contains(key) && !value.isJsonNull()) {
132
+ heuristic.add(key);
133
+ }
134
+ }
135
+ if (heuristic.isEmpty()) {
136
+ flatten.add(prefix, new JsonPrimitive(json.toString()));
137
+ }
138
+ else {
139
+ for (String key : heuristic) {
140
+ JsonElement value = json.get(key);
141
+ flatten.add(appendPrefix(prefix, key), value);
142
+ }
143
+ }
144
+ }
145
+
146
+ private String appendPrefix(String prefix, String key)
147
+ {
148
+ return prefix.isEmpty() ? key : prefix + "." + key;
149
+ }
150
+ }
@@ -0,0 +1,226 @@
1
+ package org.embulk.input.jira;
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.collect.ImmutableList;
7
+ import com.google.gson.JsonArray;
8
+ import com.google.gson.JsonElement;
9
+ import com.google.gson.JsonNull;
10
+ import com.google.gson.JsonObject;
11
+
12
+ import org.embulk.config.Config;
13
+ import org.embulk.config.ConfigDefault;
14
+ import org.embulk.config.ConfigDiff;
15
+ import org.embulk.config.ConfigException;
16
+ import org.embulk.config.ConfigSource;
17
+ import org.embulk.config.Task;
18
+ import org.embulk.config.TaskReport;
19
+ import org.embulk.config.TaskSource;
20
+ import org.embulk.exec.GuessExecutor;
21
+ import org.embulk.input.jira.client.JiraClient;
22
+ import org.embulk.input.jira.util.JiraUtil;
23
+ import org.embulk.spi.Buffer;
24
+ import org.embulk.spi.Exec;
25
+ import org.embulk.spi.InputPlugin;
26
+ import org.embulk.spi.PageBuilder;
27
+ import org.embulk.spi.PageOutput;
28
+ import org.embulk.spi.Schema;
29
+ import org.embulk.spi.SchemaConfig;
30
+ import org.slf4j.Logger;
31
+
32
+ import java.util.List;
33
+ import java.util.Map.Entry;
34
+ import java.util.Set;
35
+ import java.util.SortedSet;
36
+ import java.util.TreeSet;
37
+
38
+ import static org.embulk.input.jira.Constant.GUESS_BUFFER_SIZE;
39
+ import static org.embulk.input.jira.Constant.GUESS_RECORDS_COUNT;
40
+ import static org.embulk.input.jira.Constant.MAX_RESULTS;
41
+ import static org.embulk.input.jira.Constant.PREVIEW_RECORDS_COUNT;
42
+
43
+ public class JiraInputPlugin
44
+ implements InputPlugin
45
+ {
46
+ private static final Logger LOGGER = Exec.getLogger(JiraInputPlugin.class);
47
+
48
+ public interface PluginTask
49
+ extends Task
50
+ {
51
+ @Config("username")
52
+ public String getUsername();
53
+
54
+ @Config("password")
55
+ public String getPassword();
56
+
57
+ @Config("uri")
58
+ public String getUri();
59
+
60
+ @Config("initial_retry_interval_millis")
61
+ @ConfigDefault("1000")
62
+ int getInitialRetryIntervalMillis();
63
+
64
+ @Config("maximum_retry_interval_millis")
65
+ @ConfigDefault("120000")
66
+ int getMaximumRetryIntervalMillis();
67
+
68
+ @Config("timeout_millis")
69
+ @ConfigDefault("300000")
70
+ int getTimeoutMillis();
71
+
72
+ @Config("retry_limit")
73
+ @ConfigDefault("5")
74
+ public int getRetryLimit();
75
+
76
+ @Config("jql")
77
+ public String getJQL();
78
+
79
+ @Config("columns")
80
+ public SchemaConfig getColumns();
81
+
82
+ // For future support of other authentication methods
83
+ @Config("auth_method")
84
+ @ConfigDefault("\"basic\"")
85
+ public AuthenticateMethod getAuthMethod();
86
+ }
87
+
88
+ @Override
89
+ public ConfigDiff transaction(ConfigSource config,
90
+ InputPlugin.Control control)
91
+ {
92
+ PluginTask task = config.loadConfig(PluginTask.class);
93
+
94
+ Schema schema = task.getColumns().toSchema();
95
+ int taskCount = 1;
96
+
97
+ return resume(task.dump(), schema, taskCount, control);
98
+ }
99
+
100
+ @Override
101
+ public ConfigDiff resume(TaskSource taskSource,
102
+ Schema schema, int taskCount,
103
+ InputPlugin.Control control)
104
+ {
105
+ control.run(taskSource, schema, taskCount);
106
+ return Exec.newConfigDiff();
107
+ }
108
+
109
+ @Override
110
+ public void cleanup(TaskSource taskSource,
111
+ Schema schema, int taskCount,
112
+ List<TaskReport> successTaskReports)
113
+ {
114
+ }
115
+
116
+ @Override
117
+ public TaskReport run(TaskSource taskSource,
118
+ Schema schema, int taskIndex,
119
+ PageOutput output)
120
+ {
121
+ PluginTask task = taskSource.loadTask(PluginTask.class);
122
+ JiraUtil.validateTaskConfig(task);
123
+ JiraClient jiraClient = getJiraClient();
124
+ jiraClient.checkUserCredentials(task);
125
+ try (final PageBuilder pageBuilder = getPageBuilder(schema, output)) {
126
+ if (isPreview()) {
127
+ List<Issue> issues = jiraClient.searchIssues(task, 0, PREVIEW_RECORDS_COUNT);
128
+ issues.forEach(issue -> JiraUtil.addRecord(issue, schema, task, pageBuilder));
129
+ }
130
+ else {
131
+ int currentPage = 0;
132
+ int totalCount = jiraClient.getTotalCount(task);
133
+ int totalPage = JiraUtil.calculateTotalPage(totalCount, MAX_RESULTS);
134
+ LOGGER.info(String.format("Total pages (%d)", totalPage));
135
+ while (currentPage < totalPage) {
136
+ LOGGER.info(String.format("Fetching page %d/%d", (currentPage + 1), totalPage));
137
+ List<Issue> issues = jiraClient.searchIssues(task, (currentPage * MAX_RESULTS), MAX_RESULTS);
138
+ issues.forEach(issue -> JiraUtil.addRecord(issue, schema, task, pageBuilder));
139
+ currentPage++;
140
+ }
141
+ }
142
+ pageBuilder.finish();
143
+ }
144
+ return Exec.newTaskReport();
145
+ }
146
+
147
+ @Override
148
+ public ConfigDiff guess(ConfigSource config)
149
+ {
150
+ // Reset columns in case already have or missing on configuration
151
+ config.set("columns", new ObjectMapper().createArrayNode());
152
+ PluginTask task = config.loadConfig(PluginTask.class);
153
+ JiraUtil.validateTaskConfig(task);
154
+ JiraClient jiraClient = getJiraClient();
155
+ jiraClient.checkUserCredentials(task);
156
+ List<Issue> issues = jiraClient.searchIssues(task, 0, GUESS_RECORDS_COUNT);
157
+ if (issues.isEmpty()) {
158
+ throw new ConfigException("Could not guess schema due to empty data set");
159
+ }
160
+ Buffer sample = Buffer.copyOf(createSamples(issues, getUniqueAttributes(issues)).toString().getBytes());
161
+ JsonNode columns = Exec.getInjector().getInstance(GuessExecutor.class)
162
+ .guessParserConfig(sample, Exec.newConfigSource(), createGuessConfig())
163
+ .getObjectNode().get("columns");
164
+ return Exec.newConfigDiff().set("columns", columns);
165
+ }
166
+
167
+ private ConfigSource createGuessConfig()
168
+ {
169
+ return Exec.newConfigSource()
170
+ .set("guess_plugins", ImmutableList.of("jira"))
171
+ .set("guess_sample_buffer_bytes", GUESS_BUFFER_SIZE);
172
+ }
173
+
174
+ private SortedSet<String> getUniqueAttributes(List<Issue> issues)
175
+ {
176
+ SortedSet<String> uniqueAttributes = new TreeSet<>();
177
+ for (Issue issue : issues) {
178
+ for (Entry<String, JsonElement> entry : issue.getFlatten().entrySet()) {
179
+ uniqueAttributes.add(entry.getKey());
180
+ }
181
+ }
182
+ return uniqueAttributes;
183
+ }
184
+
185
+ private JsonArray createSamples(List<Issue> issues, Set<String> uniqueAttributes)
186
+ {
187
+ JsonArray samples = new JsonArray();
188
+ for (Issue issue : issues) {
189
+ JsonObject flatten = issue.getFlatten();
190
+ JsonObject unified = new JsonObject();
191
+ for (String key : uniqueAttributes) {
192
+ JsonElement value = flatten.get(key);
193
+ if (value == null) {
194
+ value = JsonNull.INSTANCE;
195
+ }
196
+ unified.add(key, value);
197
+ }
198
+ samples.add(unified);
199
+ }
200
+ return samples;
201
+ }
202
+
203
+ @VisibleForTesting
204
+ public GuessExecutor getGuessExecutor()
205
+ {
206
+ return Exec.getInjector().getInstance(GuessExecutor.class);
207
+ }
208
+
209
+ @VisibleForTesting
210
+ public PageBuilder getPageBuilder(Schema schema, PageOutput output)
211
+ {
212
+ return new PageBuilder(Exec.getBufferAllocator(), schema, output);
213
+ }
214
+
215
+ @VisibleForTesting
216
+ public boolean isPreview()
217
+ {
218
+ return Exec.isPreview();
219
+ }
220
+
221
+ @VisibleForTesting
222
+ public JiraClient getJiraClient()
223
+ {
224
+ return new JiraClient();
225
+ }
226
+ }
@@ -0,0 +1,254 @@
1
+ package org.embulk.input.jira.client;
2
+
3
+ import com.google.common.annotations.VisibleForTesting;
4
+ import com.google.gson.JsonArray;
5
+ import com.google.gson.JsonElement;
6
+ import com.google.gson.JsonObject;
7
+ import com.google.gson.JsonParser;
8
+ import com.google.gson.JsonPrimitive;
9
+
10
+ import org.apache.http.HttpResponse;
11
+ import org.apache.http.HttpStatus;
12
+ import org.apache.http.client.HttpClient;
13
+ import org.apache.http.client.config.RequestConfig;
14
+ import org.apache.http.client.methods.HttpGet;
15
+ import org.apache.http.client.methods.HttpPost;
16
+ import org.apache.http.client.methods.HttpRequestBase;
17
+ import org.apache.http.entity.StringEntity;
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.jira.Issue;
22
+ import org.embulk.input.jira.JiraInputPlugin.PluginTask;
23
+ import org.embulk.input.jira.util.JiraException;
24
+ import org.embulk.input.jira.util.JiraUtil;
25
+ import org.embulk.spi.Exec;
26
+ import org.embulk.spi.util.RetryExecutor.RetryGiveupException;
27
+ import org.embulk.spi.util.RetryExecutor.Retryable;
28
+ import org.slf4j.Logger;
29
+
30
+ import java.io.IOException;
31
+ import java.util.ArrayList;
32
+ import java.util.List;
33
+ import java.util.Map.Entry;
34
+ import java.util.Set;
35
+ import java.util.stream.Collectors;
36
+ import java.util.stream.StreamSupport;
37
+
38
+ import static java.util.Base64.getEncoder;
39
+ import static org.apache.http.HttpHeaders.ACCEPT;
40
+ import static org.apache.http.HttpHeaders.AUTHORIZATION;
41
+ import static org.apache.http.HttpHeaders.CONTENT_TYPE;
42
+ import static org.embulk.input.jira.Constant.MIN_RESULTS;
43
+ import static org.embulk.spi.util.RetryExecutor.retryExecutor;
44
+
45
+ public class JiraClient
46
+ {
47
+ public JiraClient() {}
48
+
49
+ private static final int CONNECTION_TIME_OUT = 300000;
50
+
51
+ private static final Logger LOGGER = Exec.getLogger(JiraClient.class);
52
+
53
+ public void checkUserCredentials(final PluginTask task)
54
+ {
55
+ try {
56
+ authorizeAndRequest(task, JiraUtil.buildPermissionUrl(task.getUri()), null);
57
+ }
58
+ catch (JiraException e) {
59
+ LOGGER.error(String.format("JIRA return status (%s), reason (%s)", e.getStatusCode(), e.getMessage()));
60
+ if (e.getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
61
+ throw new ConfigException("Could not authorize with your credential.");
62
+ }
63
+ else {
64
+ throw new ConfigException("Could not authorize with your credential due to problems when contacting JIRA API.");
65
+ }
66
+ }
67
+ }
68
+
69
+ public List<Issue> searchIssues(final PluginTask task, int startAt, int maxResults)
70
+ {
71
+ String response = searchJiraAPI(task, startAt, maxResults);
72
+ JsonObject result = new JsonParser().parse(response).getAsJsonObject();
73
+ return StreamSupport.stream(result.get("issues").getAsJsonArray().spliterator(), false)
74
+ .map(jsonElement -> {
75
+ JsonObject json = jsonElement.getAsJsonObject();
76
+ JsonObject fields = json.get("fields").getAsJsonObject();
77
+ Set<Entry<String, JsonElement>> entries = fields.entrySet();
78
+ json.remove("fields");
79
+ // Merged all properties in fields to the object
80
+ for (Entry<String, JsonElement> entry : entries) {
81
+ json.add(entry.getKey(), entry.getValue());
82
+ }
83
+ return new Issue(json);
84
+ })
85
+ .collect(Collectors.toList());
86
+ }
87
+
88
+ public int getTotalCount(final PluginTask task)
89
+ {
90
+ return new JsonParser().parse(searchJiraAPI(task, 0, MIN_RESULTS)).getAsJsonObject().get("total").getAsInt();
91
+ }
92
+
93
+ private String searchJiraAPI(final PluginTask task, int startAt, int maxResults)
94
+ {
95
+ try {
96
+ return retryExecutor().withRetryLimit(task.getRetryLimit())
97
+ .withInitialRetryWait(task.getInitialRetryIntervalMillis())
98
+ .withMaxRetryWait(task.getMaximumRetryIntervalMillis())
99
+ .runInterruptible(new Retryable<String>()
100
+ {
101
+ @Override
102
+ public String call() throws Exception
103
+ {
104
+ return authorizeAndRequest(task, JiraUtil.buildSearchUrl(task.getUri()), createSearchIssuesBody(task, startAt, maxResults));
105
+ }
106
+
107
+ @Override
108
+ public boolean isRetryableException(Exception exception)
109
+ {
110
+ if (exception instanceof JiraException) {
111
+ int statusCode = ((JiraException) exception).getStatusCode();
112
+ // When overloading JIRA APIs (i.e 100 requests per second) the API will return 401 although the credential is correct. So add retry for this
113
+ // 429 is stand for "Too many requests"
114
+ // Other 4xx considered errors
115
+ return statusCode / 100 != 4 || statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == 429;
116
+ }
117
+ return false;
118
+ }
119
+
120
+ @Override
121
+ public void onRetry(Exception exception, int retryCount, int retryLimit, int retryWait)
122
+ throws RetryGiveupException
123
+ {
124
+ if (exception instanceof JiraException) {
125
+ String message = String
126
+ .format("Retrying %d/%d after %d seconds. HTTP status code: %s",
127
+ retryCount, retryLimit,
128
+ retryWait / 1000,
129
+ ((JiraException) exception).getStatusCode());
130
+ LOGGER.warn(message);
131
+ }
132
+ else {
133
+ String message = String
134
+ .format("Retrying %d/%d after %d seconds. Message: %s",
135
+ retryCount, retryLimit,
136
+ retryWait / 1000,
137
+ exception.getMessage());
138
+ LOGGER.warn(message, exception);
139
+ }
140
+ }
141
+
142
+ @Override
143
+ public void onGiveup(Exception firstException, Exception lastException) throws RetryGiveupException
144
+ {
145
+ LOGGER.warn("Retry Limit Exceeded");
146
+ }
147
+ });
148
+ }
149
+ catch (RetryGiveupException | InterruptedException e) {
150
+ if (e instanceof RetryGiveupException && e.getCause() != null && e.getCause() instanceof JiraException) {
151
+ throw new ConfigException(e.getCause().getMessage());
152
+ }
153
+ throw new ConfigException(e);
154
+ }
155
+ }
156
+
157
+ private String authorizeAndRequest(final PluginTask task, String url, String body) throws JiraException
158
+ {
159
+ try {
160
+ HttpClient client = createHttpClient();
161
+ HttpRequestBase request;
162
+ if (body == null) {
163
+ request = createGetRequest(task, url);
164
+ }
165
+ else {
166
+ request = createPostRequest(task, url, body);
167
+ }
168
+ HttpResponse response = client.execute(request);
169
+ // Check for HTTP response code : 200 : SUCCESS
170
+ int statusCode = response.getStatusLine().getStatusCode();
171
+ if (statusCode != HttpStatus.SC_OK) {
172
+ throw new JiraException(statusCode, extractErrorMessages(EntityUtils.toString(response.getEntity())));
173
+ }
174
+ return EntityUtils.toString(response.getEntity());
175
+ }
176
+ catch (IOException e) {
177
+ throw new JiraException(-1, e.getMessage());
178
+ }
179
+ }
180
+
181
+ private String extractErrorMessages(String errorResponse)
182
+ {
183
+ List<String> messages = new ArrayList<>();
184
+ try {
185
+ JsonObject errorObject = new JsonParser().parse(errorResponse).getAsJsonObject();
186
+ for (JsonElement element : errorObject.get("errorMessages").getAsJsonArray()) {
187
+ messages.add(element.getAsString());
188
+ }
189
+ }
190
+ catch (Exception e) {
191
+ messages.add(errorResponse);
192
+ }
193
+ return String.join(" , ", messages);
194
+ }
195
+
196
+ @VisibleForTesting
197
+ public HttpClient createHttpClient()
198
+ {
199
+ RequestConfig config = RequestConfig.custom()
200
+ .setConnectTimeout(CONNECTION_TIME_OUT)
201
+ .setConnectionRequestTimeout(CONNECTION_TIME_OUT)
202
+ .build();
203
+ return HttpClientBuilder.create().setDefaultRequestConfig(config).build();
204
+ }
205
+
206
+ private HttpRequestBase createPostRequest(PluginTask task, String url, String body) throws IOException
207
+ {
208
+ HttpPost request = new HttpPost(url);
209
+ switch (task.getAuthMethod()) {
210
+ default:
211
+ request.setHeader(
212
+ AUTHORIZATION,
213
+ String.format("Basic %s",
214
+ getEncoder().encodeToString(String.format("%s:%s",
215
+ task.getUsername(),
216
+ task.getPassword()).getBytes())));
217
+ request.setHeader(ACCEPT, "application/json");
218
+ request.setHeader(CONTENT_TYPE, "application/json");
219
+ break;
220
+ }
221
+ request.setEntity(new StringEntity(body));
222
+ return request;
223
+ }
224
+
225
+ private HttpRequestBase createGetRequest(PluginTask task, String url)
226
+ {
227
+ HttpGet request = new HttpGet(url);
228
+ switch (task.getAuthMethod()) {
229
+ default:
230
+ request.setHeader(
231
+ AUTHORIZATION,
232
+ String.format("Basic %s",
233
+ getEncoder().encodeToString(String.format("%s:%s",
234
+ task.getUsername(),
235
+ task.getPassword()).getBytes())));
236
+ request.setHeader(ACCEPT, "application/json");
237
+ request.setHeader(CONTENT_TYPE, "application/json");
238
+ break;
239
+ }
240
+ return request;
241
+ }
242
+
243
+ private String createSearchIssuesBody(PluginTask task, int startAt, int maxResults)
244
+ {
245
+ JsonObject body = new JsonObject();
246
+ body.add("jql", new JsonPrimitive(task.getJQL()));
247
+ body.add("startAt", new JsonPrimitive(startAt));
248
+ body.add("maxResults", new JsonPrimitive(maxResults));
249
+ JsonArray fields = new JsonArray();
250
+ fields.add("*all");
251
+ body.add("fields", fields);
252
+ return body.toString();
253
+ }
254
+ }