embulk-output-kintone 0.4.0 → 1.0.0
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/.github/workflows/ci.yml +1 -0
- data/.gitignore +0 -1
- data/README.md +8 -6
- data/build.gradle +18 -19
- data/classpath/embulk-output-kintone-1.0.0.jar +0 -0
- data/classpath/shadow-kintone-java-client-1.0.0-all.jar +0 -0
- data/gradle/wrapper/gradle-wrapper.properties +1 -1
- data/settings.gradle +2 -0
- data/shadow-kintone-java-client/build.gradle +35 -0
- data/src/main/java/org/embulk/output/kintone/KintoneColumnOption.java +1 -2
- data/src/main/java/org/embulk/output/kintone/KintoneColumnType.java +368 -0
- data/src/main/java/org/embulk/output/kintone/KintoneColumnVisitor.java +195 -135
- data/src/main/java/org/embulk/output/kintone/KintoneMode.java +0 -1
- data/src/main/java/org/embulk/output/kintone/KintoneOutputPlugin.java +0 -2
- data/src/main/java/org/embulk/output/kintone/KintonePageOutput.java +249 -192
- data/src/main/java/org/embulk/output/kintone/KintoneRetryOption.java +19 -0
- data/src/main/java/org/embulk/output/kintone/PluginTask.java +11 -3
- data/src/test/java/com/kintone/client/Json.java +16 -0
- data/src/test/java/org/embulk/output/kintone/KintoneColumnOptionBuilder.java +62 -0
- data/src/test/java/org/embulk/output/kintone/KintoneColumnVisitorTest.java +820 -0
- data/src/test/java/org/embulk/output/kintone/KintoneColumnVisitorVerifier.java +73 -0
- data/src/test/java/org/embulk/output/kintone/KintonePageOutputVerifier.java +221 -0
- data/src/test/java/org/embulk/output/kintone/OutputPageBuilder.java +92 -0
- data/src/test/java/org/embulk/output/kintone/TestKintoneOutputPlugin.java +176 -1
- data/src/test/java/org/embulk/output/kintone/TestTask.java +40 -0
- data/src/test/java/org/embulk/output/kintone/TestTaskMode.java +43 -0
- data/src/test/resources/logback-test.xml +14 -0
- data/src/test/resources/org/embulk/output/kintone/config.yml +4 -0
- data/src/test/resources/org/embulk/output/kintone/task/config.yml +1 -0
- data/src/test/resources/org/embulk/output/kintone/task/mode/config.yml +158 -0
- data/src/test/resources/org/embulk/output/kintone/task/mode/input.csv +7 -0
- data/src/test/resources/org/embulk/output/kintone/task/mode/insert_add_ignore_nulls_records.jsonl +6 -0
- data/src/test/resources/org/embulk/output/kintone/task/mode/insert_add_prefer_nulls_records.jsonl +6 -0
- data/src/test/resources/org/embulk/output/kintone/task/mode/insert_add_records.jsonl +6 -0
- data/src/test/resources/org/embulk/output/kintone/task/mode/update_update_ignore_nulls_records.jsonl +3 -0
- data/src/test/resources/org/embulk/output/kintone/task/mode/update_update_prefer_nulls_records.jsonl +3 -0
- data/src/test/resources/org/embulk/output/kintone/task/mode/update_update_records.jsonl +6 -0
- data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_add_ignore_nulls_records.jsonl +3 -0
- data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_add_prefer_nulls_records.jsonl +3 -0
- data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_add_records.jsonl +2 -0
- data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_update_ignore_nulls_records.jsonl +3 -0
- data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_update_prefer_nulls_records.jsonl +3 -0
- data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_update_records.jsonl +4 -0
- data/src/test/resources/org/embulk/output/kintone/task/mode/values.json +1 -0
- data/src/test/resources/org/embulk/output/kintone/task/mode/values_ignore_nulls.json +1 -0
- data/src/test/resources/org/embulk/output/kintone/task/mode/values_prefer_nulls.json +1 -0
- metadata +36 -3
- data/classpath/embulk-output-kintone-0.4.0.jar +0 -0
@@ -1,41 +1,65 @@
|
|
1
1
|
package org.embulk.output.kintone;
|
2
2
|
|
3
|
+
import static org.embulk.spi.util.RetryExecutor.retryExecutor;
|
4
|
+
|
5
|
+
import com.fasterxml.jackson.databind.JsonNode;
|
6
|
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
7
|
+
import com.google.common.collect.Maps;
|
3
8
|
import com.kintone.client.KintoneClient;
|
4
9
|
import com.kintone.client.KintoneClientBuilder;
|
5
10
|
import com.kintone.client.api.record.GetRecordsByCursorResponseBody;
|
11
|
+
import com.kintone.client.exception.KintoneApiRuntimeException;
|
12
|
+
import com.kintone.client.model.app.field.FieldProperty;
|
6
13
|
import com.kintone.client.model.record.FieldType;
|
7
14
|
import com.kintone.client.model.record.Record;
|
8
15
|
import com.kintone.client.model.record.RecordForUpdate;
|
9
16
|
import com.kintone.client.model.record.UpdateKey;
|
17
|
+
import java.io.IOException;
|
18
|
+
import java.lang.invoke.MethodHandles;
|
10
19
|
import java.math.BigDecimal;
|
11
20
|
import java.util.ArrayList;
|
12
21
|
import java.util.Arrays;
|
13
22
|
import java.util.Collections;
|
14
23
|
import java.util.List;
|
15
|
-
import java.util.
|
24
|
+
import java.util.Map;
|
25
|
+
import java.util.Objects;
|
26
|
+
import java.util.TreeMap;
|
27
|
+
import java.util.function.Function;
|
28
|
+
import java.util.function.Supplier;
|
16
29
|
import java.util.stream.Collectors;
|
30
|
+
import org.apache.commons.lang3.tuple.Pair;
|
17
31
|
import org.embulk.config.ConfigException;
|
18
32
|
import org.embulk.config.TaskReport;
|
19
|
-
import org.embulk.spi.Column;
|
20
33
|
import org.embulk.spi.Exec;
|
21
34
|
import org.embulk.spi.Page;
|
22
35
|
import org.embulk.spi.PageReader;
|
23
36
|
import org.embulk.spi.Schema;
|
24
37
|
import org.embulk.spi.TransactionalPageOutput;
|
38
|
+
import org.embulk.spi.util.RetryExecutor.RetryGiveupException;
|
39
|
+
import org.embulk.spi.util.RetryExecutor.Retryable;
|
25
40
|
import org.slf4j.Logger;
|
26
41
|
import org.slf4j.LoggerFactory;
|
27
42
|
|
28
43
|
public class KintonePageOutput implements TransactionalPageOutput {
|
29
|
-
|
30
|
-
|
31
|
-
private static final
|
32
|
-
|
44
|
+
private static final Logger LOGGER =
|
45
|
+
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
|
46
|
+
private static final List<String> RETRYABLE_ERROR_CODES =
|
47
|
+
Arrays.asList(
|
48
|
+
"GAIA_TM12", // 作成できるカーソルの上限に達しているため、カーソルを作成できません。不要なカーソルを削除するか、しばらく経ってから再実行してください。
|
49
|
+
"GAIA_RE18", // データベースのロックに失敗したため、変更を保存できませんでした。時間をおいて再度お試しください。
|
50
|
+
"GAIA_DA02" // データベースのロックに失敗したため、変更を保存できませんでした。時間をおいて再度お試しください。
|
51
|
+
);
|
52
|
+
private static final int UPSERT_BATCH_SIZE = 10000;
|
53
|
+
private static final int CHUNK_SIZE = 100;
|
54
|
+
private final Map<String, Pair<FieldType, FieldType>> wrongTypeFields = new TreeMap<>();
|
33
55
|
private final PluginTask task;
|
56
|
+
private final PageReader reader;
|
34
57
|
private KintoneClient client;
|
58
|
+
private Map<String, FieldProperty> formFields;
|
35
59
|
|
36
60
|
public KintonePageOutput(PluginTask task, Schema schema) {
|
37
|
-
this.pageReader = new PageReader(schema);
|
38
61
|
this.task = task;
|
62
|
+
reader = new PageReader(schema);
|
39
63
|
}
|
40
64
|
|
41
65
|
@Override
|
@@ -63,11 +87,11 @@ public class KintonePageOutput implements TransactionalPageOutput {
|
|
63
87
|
|
64
88
|
@Override
|
65
89
|
public void close() {
|
66
|
-
if (
|
67
|
-
return;
|
90
|
+
if (client == null) {
|
91
|
+
return; // Not connected
|
68
92
|
}
|
69
93
|
try {
|
70
|
-
|
94
|
+
client.close();
|
71
95
|
} catch (Exception e) {
|
72
96
|
throw new RuntimeException("kintone throw exception", e);
|
73
97
|
}
|
@@ -80,228 +104,244 @@ public class KintonePageOutput implements TransactionalPageOutput {
|
|
80
104
|
|
81
105
|
@Override
|
82
106
|
public TaskReport commit() {
|
107
|
+
wrongTypeFields.forEach(
|
108
|
+
(key, value) ->
|
109
|
+
LOGGER.warn(
|
110
|
+
String.format(
|
111
|
+
"Field type of %s is expected %s but actual %s",
|
112
|
+
key, value.getLeft(), value.getRight())));
|
83
113
|
return Exec.newTaskReport();
|
84
114
|
}
|
85
115
|
|
86
|
-
public
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
public void connect(final PluginTask task) {
|
116
|
+
public void connectIfNeeded() {
|
117
|
+
if (client != null) {
|
118
|
+
return; // Already connected
|
119
|
+
}
|
91
120
|
KintoneClientBuilder builder = KintoneClientBuilder.create("https://" + task.getDomain());
|
92
121
|
if (task.getGuestSpaceId().isPresent()) {
|
93
|
-
builder.setGuestSpaceId(task.getGuestSpaceId().
|
122
|
+
builder.setGuestSpaceId(task.getGuestSpaceId().get());
|
94
123
|
}
|
95
124
|
if (task.getBasicAuthUsername().isPresent() && task.getBasicAuthPassword().isPresent()) {
|
96
125
|
builder.withBasicAuth(task.getBasicAuthUsername().get(), task.getBasicAuthPassword().get());
|
97
126
|
}
|
98
|
-
|
99
127
|
if (task.getUsername().isPresent() && task.getPassword().isPresent()) {
|
100
|
-
|
101
|
-
builder.authByPassword(task.getUsername().get(), task.getPassword().get()).build();
|
128
|
+
builder.authByPassword(task.getUsername().get(), task.getPassword().get());
|
102
129
|
} else if (task.getToken().isPresent()) {
|
103
|
-
|
104
|
-
}
|
105
|
-
}
|
106
|
-
|
107
|
-
private void execute(Consumer<KintoneClient> operation) {
|
108
|
-
connect(task);
|
109
|
-
if (this.client != null) {
|
110
|
-
operation.accept(this.client);
|
130
|
+
builder.authByApiToken(task.getToken().get());
|
111
131
|
} else {
|
112
|
-
throw new
|
132
|
+
throw new ConfigException("Username and password or token must be configured.");
|
113
133
|
}
|
134
|
+
client = builder.build();
|
135
|
+
formFields = client.app().getFormFields(task.getAppId());
|
114
136
|
}
|
115
137
|
|
116
|
-
private void
|
117
|
-
|
118
|
-
|
119
|
-
try {
|
120
|
-
ArrayList<Record> records = new ArrayList<>();
|
121
|
-
pageReader.setPage(page);
|
122
|
-
KintoneColumnVisitor visitor =
|
123
|
-
new KintoneColumnVisitor(pageReader, task.getColumnOptions());
|
124
|
-
while (pageReader.nextRecord()) {
|
125
|
-
Record record = new Record();
|
126
|
-
visitor.setRecord(record);
|
127
|
-
for (Column column : pageReader.getSchema().getColumns()) {
|
128
|
-
column.visit(visitor);
|
129
|
-
}
|
138
|
+
private void insert(List<Record> records) {
|
139
|
+
executeWithRetry(() -> client.record().addRecords(task.getAppId(), records));
|
140
|
+
}
|
130
141
|
|
131
|
-
|
132
|
-
|
133
|
-
client.record().addRecords(task.getAppId(), records);
|
134
|
-
records.clear();
|
135
|
-
sleep();
|
136
|
-
}
|
137
|
-
}
|
138
|
-
if (records.size() > 0) {
|
139
|
-
client.record().addRecords(task.getAppId(), records);
|
140
|
-
}
|
141
|
-
} catch (Exception e) {
|
142
|
-
throw new RuntimeException("kintone throw exception", e);
|
143
|
-
}
|
144
|
-
});
|
142
|
+
private void update(List<RecordForUpdate> records) {
|
143
|
+
executeWithRetry(() -> client.record().updateRecords(task.getAppId(), records));
|
145
144
|
}
|
146
145
|
|
147
|
-
private
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
146
|
+
private <T> T executeWithRetry(Supplier<T> operation) {
|
147
|
+
connectIfNeeded();
|
148
|
+
KintoneRetryOption retryOption = task.getRetryOptions();
|
149
|
+
try {
|
150
|
+
return retryExecutor()
|
151
|
+
.withRetryLimit(retryOption.getLimit())
|
152
|
+
.withInitialRetryWait(retryOption.getInitialWaitMillis())
|
153
|
+
.withMaxRetryWait(retryOption.getMaxWaitMillis())
|
154
|
+
.runInterruptible(
|
155
|
+
new Retryable<T>() {
|
156
|
+
@Override
|
157
|
+
public T call() throws Exception {
|
158
|
+
return operation.get();
|
159
|
+
}
|
153
160
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
161
|
+
@Override
|
162
|
+
public boolean isRetryableException(Exception exception) {
|
163
|
+
if (!(exception instanceof KintoneApiRuntimeException)) {
|
164
|
+
return false;
|
165
|
+
}
|
166
|
+
try {
|
167
|
+
ObjectMapper mapper = new ObjectMapper();
|
168
|
+
JsonNode content =
|
169
|
+
mapper.readTree(((KintoneApiRuntimeException) exception).getContent());
|
170
|
+
String code = content.get("code").textValue();
|
171
|
+
return RETRYABLE_ERROR_CODES.contains(code);
|
172
|
+
} catch (IOException e) {
|
173
|
+
throw new RuntimeException(e);
|
174
|
+
}
|
175
|
+
}
|
169
176
|
|
170
|
-
|
171
|
-
|
172
|
-
|
177
|
+
@Override
|
178
|
+
public void onRetry(
|
179
|
+
Exception exception, int retryCount, int retryLimit, int retryWait)
|
180
|
+
throws RetryGiveupException {
|
181
|
+
String message =
|
182
|
+
String.format(
|
183
|
+
"Retrying %d/%d after %d seconds. Message: %s",
|
184
|
+
retryCount, retryLimit, retryWait / 1000, exception.getMessage());
|
185
|
+
if (retryCount % 3 == 0) {
|
186
|
+
LOGGER.warn(message, exception);
|
187
|
+
} else {
|
188
|
+
LOGGER.warn(message);
|
189
|
+
}
|
190
|
+
}
|
173
191
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
}
|
182
|
-
if (updateRecords.size() > 0) {
|
183
|
-
client.record().updateRecords(task.getAppId(), updateRecords);
|
184
|
-
}
|
185
|
-
} catch (Exception e) {
|
186
|
-
throw new RuntimeException("kintone throw exception", e);
|
187
|
-
}
|
188
|
-
});
|
192
|
+
@Override
|
193
|
+
public void onGiveup(Exception firstException, Exception lastException)
|
194
|
+
throws RetryGiveupException {}
|
195
|
+
});
|
196
|
+
} catch (RetryGiveupException | InterruptedException e) {
|
197
|
+
throw new RuntimeException("kintone throw exception", e);
|
198
|
+
}
|
189
199
|
}
|
190
200
|
|
191
|
-
private void
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
201
|
+
private void insertPage(Page page) {
|
202
|
+
List<Record> records = new ArrayList<>();
|
203
|
+
reader.setPage(page);
|
204
|
+
KintoneColumnVisitor visitor =
|
205
|
+
new KintoneColumnVisitor(
|
206
|
+
reader, task.getColumnOptions(), task.getPreferNulls(), task.getIgnoreNulls());
|
207
|
+
while (reader.nextRecord()) {
|
208
|
+
Record record = new Record();
|
209
|
+
visitor.setRecord(record);
|
210
|
+
reader.getSchema().visitColumns(visitor);
|
211
|
+
putWrongTypeFields(record);
|
212
|
+
records.add(record);
|
213
|
+
if (records.size() == CHUNK_SIZE) {
|
214
|
+
insert(records);
|
215
|
+
records.clear();
|
216
|
+
}
|
217
|
+
}
|
218
|
+
if (!records.isEmpty()) {
|
219
|
+
insert(records);
|
220
|
+
}
|
221
|
+
}
|
198
222
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
223
|
+
private void updatePage(Page page) {
|
224
|
+
List<RecordForUpdate> records = new ArrayList<>();
|
225
|
+
reader.setPage(page);
|
226
|
+
KintoneColumnVisitor visitor =
|
227
|
+
new KintoneColumnVisitor(
|
228
|
+
reader,
|
229
|
+
task.getColumnOptions(),
|
230
|
+
task.getPreferNulls(),
|
231
|
+
task.getIgnoreNulls(),
|
232
|
+
task.getUpdateKeyName()
|
233
|
+
.orElseThrow(() -> new RuntimeException("unreachable"))); // Already validated
|
234
|
+
while (reader.nextRecord()) {
|
235
|
+
Record record = new Record();
|
236
|
+
UpdateKey updateKey = new UpdateKey();
|
237
|
+
visitor.setRecord(record);
|
238
|
+
visitor.setUpdateKey(updateKey);
|
239
|
+
reader.getSchema().visitColumns(visitor);
|
240
|
+
putWrongTypeFields(record);
|
241
|
+
if (updateKey.getValue() == null || updateKey.getValue().toString().isEmpty()) {
|
242
|
+
LOGGER.warn("Record skipped because no update key value was specified");
|
243
|
+
continue;
|
244
|
+
}
|
245
|
+
records.add(new RecordForUpdate(updateKey, record.removeField(updateKey.getField())));
|
246
|
+
if (records.size() == CHUNK_SIZE) {
|
247
|
+
update(records);
|
248
|
+
records.clear();
|
249
|
+
}
|
250
|
+
}
|
251
|
+
if (!records.isEmpty()) {
|
252
|
+
update(records);
|
253
|
+
}
|
254
|
+
}
|
216
255
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
256
|
+
private void upsertPage(Page page) {
|
257
|
+
List<Record> records = new ArrayList<>();
|
258
|
+
List<UpdateKey> updateKeys = new ArrayList<>();
|
259
|
+
reader.setPage(page);
|
260
|
+
KintoneColumnVisitor visitor =
|
261
|
+
new KintoneColumnVisitor(
|
262
|
+
reader,
|
263
|
+
task.getColumnOptions(),
|
264
|
+
task.getPreferNulls(),
|
265
|
+
task.getIgnoreNulls(),
|
266
|
+
task.getUpdateKeyName()
|
267
|
+
.orElseThrow(() -> new RuntimeException("unreachable"))); // Already validated
|
268
|
+
while (reader.nextRecord()) {
|
269
|
+
Record record = new Record();
|
270
|
+
UpdateKey updateKey = new UpdateKey();
|
271
|
+
visitor.setRecord(record);
|
272
|
+
visitor.setUpdateKey(updateKey);
|
273
|
+
reader.getSchema().visitColumns(visitor);
|
274
|
+
putWrongTypeFields(record);
|
275
|
+
records.add(record);
|
276
|
+
updateKeys.add(updateKey);
|
277
|
+
if (records.size() == UPSERT_BATCH_SIZE) {
|
278
|
+
upsert(records, updateKeys);
|
279
|
+
records.clear();
|
280
|
+
updateKeys.clear();
|
281
|
+
}
|
282
|
+
}
|
283
|
+
if (!records.isEmpty()) {
|
284
|
+
upsert(records, updateKeys);
|
285
|
+
}
|
230
286
|
}
|
231
287
|
|
232
|
-
private void upsert(
|
233
|
-
throws InterruptedException {
|
288
|
+
private void upsert(List<Record> records, List<UpdateKey> updateKeys) {
|
234
289
|
if (records.size() != updateKeys.size()) {
|
235
290
|
throw new RuntimeException("records.size() != updateKeys.size()");
|
236
291
|
}
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
throw new ConfigException("The update_key must be 'SINGLE_LINE_TEXT' or 'NUMBER'.");
|
241
|
-
}
|
242
|
-
|
243
|
-
List<Record> existingRecords = getExistingRecordsByUpdateKey(updateKeys);
|
244
|
-
String updateField = updateKeys.get(0).getField();
|
245
|
-
List<String> existingValues =
|
246
|
-
existingRecords.stream()
|
247
|
-
.map(
|
248
|
-
(r) -> {
|
249
|
-
switch (updateKeyFieldType) {
|
250
|
-
case SINGLE_LINE_TEXT:
|
251
|
-
String s = r.getSingleLineTextFieldValue(updateField);
|
252
|
-
return s == null ? null : s.toString();
|
253
|
-
case NUMBER:
|
254
|
-
BigDecimal bd = r.getNumberFieldValue(updateField);
|
255
|
-
return bd == null ? null : bd.toPlainString();
|
256
|
-
default:
|
257
|
-
return null;
|
258
|
-
}
|
259
|
-
})
|
260
|
-
.filter(v -> v != null)
|
261
|
-
.collect(Collectors.toList());
|
262
|
-
|
263
|
-
ArrayList<Record> insertRecords = new ArrayList<>();
|
264
|
-
ArrayList<RecordForUpdate> updateRecords = new ArrayList<>();
|
292
|
+
List<String> existingValues = executeWithRetry(() -> getExistingValuesByUpdateKey(updateKeys));
|
293
|
+
List<Record> insertRecords = new ArrayList<>();
|
294
|
+
List<RecordForUpdate> updateRecords = new ArrayList<>();
|
265
295
|
for (int i = 0; i < records.size(); i++) {
|
266
296
|
Record record = records.get(i);
|
267
297
|
UpdateKey updateKey = updateKeys.get(i);
|
268
|
-
|
269
298
|
if (existsRecord(existingValues, updateKey)) {
|
270
|
-
record.removeField(updateKey.getField());
|
271
|
-
updateRecords.add(new RecordForUpdate(updateKey, record));
|
299
|
+
updateRecords.add(new RecordForUpdate(updateKey, record.removeField(updateKey.getField())));
|
272
300
|
} else {
|
273
301
|
insertRecords.add(record);
|
274
302
|
}
|
275
|
-
|
276
303
|
if (insertRecords.size() == CHUNK_SIZE) {
|
277
|
-
|
304
|
+
insert(insertRecords);
|
278
305
|
insertRecords.clear();
|
279
|
-
sleep();
|
280
306
|
} else if (updateRecords.size() == CHUNK_SIZE) {
|
281
|
-
|
307
|
+
update(updateRecords);
|
282
308
|
updateRecords.clear();
|
283
|
-
sleep();
|
284
309
|
}
|
285
310
|
}
|
286
|
-
if (insertRecords.
|
287
|
-
|
311
|
+
if (!insertRecords.isEmpty()) {
|
312
|
+
insert(insertRecords);
|
288
313
|
}
|
289
|
-
if (updateRecords.
|
290
|
-
|
314
|
+
if (!updateRecords.isEmpty()) {
|
315
|
+
update(updateRecords);
|
291
316
|
}
|
292
317
|
}
|
293
318
|
|
294
|
-
private List<
|
295
|
-
String fieldCode =
|
319
|
+
private List<String> getExistingValuesByUpdateKey(List<UpdateKey> updateKeys) {
|
320
|
+
String fieldCode =
|
321
|
+
updateKeys.stream()
|
322
|
+
.map(UpdateKey::getField)
|
323
|
+
.filter(Objects::nonNull)
|
324
|
+
.findFirst()
|
325
|
+
.orElse(null);
|
326
|
+
if (fieldCode == null) {
|
327
|
+
return Collections.emptyList();
|
328
|
+
}
|
329
|
+
Function<Record, String> fieldValueAsString;
|
330
|
+
FieldType fieldType = getFieldType(fieldCode);
|
331
|
+
if (fieldType == FieldType.SINGLE_LINE_TEXT) {
|
332
|
+
fieldValueAsString = record -> record.getSingleLineTextFieldValue(fieldCode);
|
333
|
+
} else if (fieldType == FieldType.NUMBER) {
|
334
|
+
fieldValueAsString = record -> toString(record.getNumberFieldValue(fieldCode));
|
335
|
+
} else {
|
336
|
+
throw new ConfigException("The update_key must be 'SINGLE_LINE_TEXT' or 'NUMBER'.");
|
337
|
+
}
|
296
338
|
List<String> queryValues =
|
297
339
|
updateKeys.stream()
|
298
|
-
.filter(k -> k.getValue() !=
|
299
|
-
.map(k -> "\"" + k.getValue()
|
340
|
+
.filter(k -> k.getValue() != null && !k.getValue().toString().isEmpty())
|
341
|
+
.map(k -> "\"" + k.getValue() + "\"")
|
300
342
|
.collect(Collectors.toList());
|
301
|
-
|
302
|
-
List<Record> allRecords = new ArrayList<>();
|
303
343
|
if (queryValues.isEmpty()) {
|
304
|
-
return
|
344
|
+
return Collections.emptyList();
|
305
345
|
}
|
306
346
|
String cursorId =
|
307
347
|
client
|
@@ -310,28 +350,45 @@ public class KintonePageOutput implements TransactionalPageOutput {
|
|
310
350
|
task.getAppId(),
|
311
351
|
Collections.singletonList(fieldCode),
|
312
352
|
fieldCode + " in (" + String.join(",", queryValues) + ")");
|
353
|
+
List<Record> allRecords = new ArrayList<>();
|
313
354
|
while (true) {
|
314
355
|
GetRecordsByCursorResponseBody resp = client.record().getRecordsByCursor(cursorId);
|
315
356
|
List<Record> records = resp.getRecords();
|
316
357
|
allRecords.addAll(records);
|
317
|
-
|
318
358
|
if (!resp.hasNext()) {
|
319
359
|
break;
|
320
360
|
}
|
321
361
|
}
|
322
|
-
return allRecords
|
362
|
+
return allRecords.stream()
|
363
|
+
.map(fieldValueAsString)
|
364
|
+
.filter(Objects::nonNull)
|
365
|
+
.collect(Collectors.toList());
|
323
366
|
}
|
324
367
|
|
325
|
-
private boolean existsRecord(List<String>
|
326
|
-
|
368
|
+
private boolean existsRecord(List<String> existingValues, UpdateKey updateKey) {
|
369
|
+
String value = toString(updateKey.getValue());
|
370
|
+
return value != null && existingValues.stream().anyMatch(v -> v.equals(value));
|
327
371
|
}
|
328
372
|
|
329
|
-
private void
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
373
|
+
private void putWrongTypeFields(Record record) {
|
374
|
+
record.getFieldCodes(true).stream()
|
375
|
+
.map(
|
376
|
+
fieldCode ->
|
377
|
+
Maps.immutableEntry(
|
378
|
+
fieldCode, Pair.of(record.getFieldType(fieldCode), getFieldType(fieldCode))))
|
379
|
+
.filter(entry -> entry.getValue().getLeft() != entry.getValue().getRight())
|
380
|
+
.forEach(entry -> wrongTypeFields.put(entry.getKey(), entry.getValue()));
|
381
|
+
}
|
382
|
+
|
383
|
+
private FieldType getFieldType(String fieldCode) {
|
384
|
+
connectIfNeeded();
|
385
|
+
FieldProperty field = formFields.get(fieldCode);
|
386
|
+
return field == null ? null : field.getType();
|
387
|
+
}
|
388
|
+
|
389
|
+
private static String toString(Object value) {
|
390
|
+
return value == null
|
391
|
+
? null
|
392
|
+
: value instanceof BigDecimal ? ((BigDecimal) value).toPlainString() : value.toString();
|
336
393
|
}
|
337
394
|
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
package org.embulk.output.kintone;
|
2
|
+
|
3
|
+
import org.embulk.config.Config;
|
4
|
+
import org.embulk.config.ConfigDefault;
|
5
|
+
import org.embulk.config.Task;
|
6
|
+
|
7
|
+
public interface KintoneRetryOption extends Task {
|
8
|
+
@Config("limit")
|
9
|
+
@ConfigDefault("10")
|
10
|
+
Integer getLimit();
|
11
|
+
|
12
|
+
@Config("initial_wait_millis")
|
13
|
+
@ConfigDefault("1000")
|
14
|
+
Integer getInitialWaitMillis();
|
15
|
+
|
16
|
+
@Config("max_wait_millis")
|
17
|
+
@ConfigDefault("60000")
|
18
|
+
Integer getMaxWaitMillis();
|
19
|
+
}
|
@@ -41,6 +41,14 @@ public interface PluginTask extends Task {
|
|
41
41
|
@ConfigDefault("{}")
|
42
42
|
Map<String, KintoneColumnOption> getColumnOptions();
|
43
43
|
|
44
|
+
@Config("prefer_nulls")
|
45
|
+
@ConfigDefault("\"false\"")
|
46
|
+
boolean getPreferNulls();
|
47
|
+
|
48
|
+
@Config("ignore_nulls")
|
49
|
+
@ConfigDefault("\"false\"")
|
50
|
+
boolean getIgnoreNulls();
|
51
|
+
|
44
52
|
@Config("mode")
|
45
53
|
@ConfigDefault("\"insert\"")
|
46
54
|
String getMode();
|
@@ -49,7 +57,7 @@ public interface PluginTask extends Task {
|
|
49
57
|
@ConfigDefault("null")
|
50
58
|
Optional<String> getUpdateKeyName();
|
51
59
|
|
52
|
-
@Config("
|
53
|
-
@ConfigDefault("
|
54
|
-
|
60
|
+
@Config("retry_options")
|
61
|
+
@ConfigDefault("{}")
|
62
|
+
KintoneRetryOption getRetryOptions();
|
55
63
|
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
package com.kintone.client;
|
2
|
+
|
3
|
+
import java.io.ByteArrayInputStream;
|
4
|
+
import java.nio.charset.StandardCharsets;
|
5
|
+
|
6
|
+
public class Json {
|
7
|
+
private static final JsonMapper MAPPER = new JsonMapper();
|
8
|
+
|
9
|
+
public static String format(Object o) {
|
10
|
+
return new String(MAPPER.format(o), StandardCharsets.UTF_8);
|
11
|
+
}
|
12
|
+
|
13
|
+
public static <T> T parse(String s, Class<T> clazz) {
|
14
|
+
return MAPPER.parse(new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)), clazz);
|
15
|
+
}
|
16
|
+
}
|