embulk-input-jira 0.2.5 → 0.2.6

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.
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
+ }