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.
- checksums.yaml +4 -4
- data/.gitignore +10 -5
- data/.travis.yml +4 -34
- data/CHANGELOG.md +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +5 -4
- data/build.gradle +116 -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/jira.rb +24 -0
- data/lib/embulk/input/jira.rb +3 -169
- data/src/main/java/org/embulk/input/jira/AuthenticateMethod.java +27 -0
- data/src/main/java/org/embulk/input/jira/Constant.java +17 -0
- data/src/main/java/org/embulk/input/jira/Issue.java +150 -0
- data/src/main/java/org/embulk/input/jira/JiraInputPlugin.java +226 -0
- data/src/main/java/org/embulk/input/jira/client/JiraClient.java +254 -0
- data/src/main/java/org/embulk/input/jira/util/JiraException.java +18 -0
- data/src/main/java/org/embulk/input/jira/util/JiraUtil.java +264 -0
- data/src/test/java/org/embulk/input/jira/IssueTest.java +278 -0
- data/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java +204 -0
- data/src/test/java/org/embulk/input/jira/JiraPluginTestRuntime.java +133 -0
- data/src/test/java/org/embulk/input/jira/TestHelpers.java +41 -0
- data/src/test/java/org/embulk/input/jira/client/JiraClientTest.java +222 -0
- data/src/test/java/org/embulk/input/jira/util/JiraUtilTest.java +318 -0
- data/src/test/resources/config.yml +13 -0
- data/src/test/resources/issue_flatten.json +129 -0
- data/src/test/resources/issue_flatten_expected.json +73 -0
- data/src/test/resources/issue_get.json +36 -0
- data/src/test/resources/issue_get_expected.json +62 -0
- data/src/test/resources/jira_client.json +81 -0
- data/src/test/resources/jira_input_plugin.json +114 -0
- data/src/test/resources/jira_util.json +26 -0
- metadata +55 -175
- data/Gemfile +0 -3
- data/LICENSE +0 -13
- data/Rakefile +0 -15
- data/embulk-input-jira.gemspec +0 -27
- data/gemfiles/embulk-0.8.0-latest +0 -4
- data/gemfiles/embulk-0.8.7 +0 -4
- data/gemfiles/embulk-0.8.8 +0 -4
- data/gemfiles/embulk-latest +0 -4
- data/gemfiles/template.erb +0 -4
- data/lib/embulk/input/jira_api.rb +0 -9
- data/lib/embulk/input/jira_api/client.rb +0 -144
- data/lib/embulk/input/jira_api/issue.rb +0 -133
- data/lib/embulk/input/jira_input_plugin_utils.rb +0 -58
- data/spec/embulk/input/jira-input-plugin-utils_spec.rb +0 -89
- data/spec/embulk/input/jira_api/client_spec.rb +0 -224
- data/spec/embulk/input/jira_api/issue_spec.rb +0 -394
- data/spec/embulk/input/jira_spec.rb +0 -322
- data/spec/embulk_spec.rb +0 -32
- data/spec/spec_helper.rb +0 -26
- 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
|
+
}
|