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