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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -0
  3. data/.gitignore +0 -1
  4. data/README.md +8 -6
  5. data/build.gradle +18 -19
  6. data/classpath/embulk-output-kintone-1.0.0.jar +0 -0
  7. data/classpath/shadow-kintone-java-client-1.0.0-all.jar +0 -0
  8. data/gradle/wrapper/gradle-wrapper.properties +1 -1
  9. data/settings.gradle +2 -0
  10. data/shadow-kintone-java-client/build.gradle +35 -0
  11. data/src/main/java/org/embulk/output/kintone/KintoneColumnOption.java +1 -2
  12. data/src/main/java/org/embulk/output/kintone/KintoneColumnType.java +368 -0
  13. data/src/main/java/org/embulk/output/kintone/KintoneColumnVisitor.java +195 -135
  14. data/src/main/java/org/embulk/output/kintone/KintoneMode.java +0 -1
  15. data/src/main/java/org/embulk/output/kintone/KintoneOutputPlugin.java +0 -2
  16. data/src/main/java/org/embulk/output/kintone/KintonePageOutput.java +249 -192
  17. data/src/main/java/org/embulk/output/kintone/KintoneRetryOption.java +19 -0
  18. data/src/main/java/org/embulk/output/kintone/PluginTask.java +11 -3
  19. data/src/test/java/com/kintone/client/Json.java +16 -0
  20. data/src/test/java/org/embulk/output/kintone/KintoneColumnOptionBuilder.java +62 -0
  21. data/src/test/java/org/embulk/output/kintone/KintoneColumnVisitorTest.java +820 -0
  22. data/src/test/java/org/embulk/output/kintone/KintoneColumnVisitorVerifier.java +73 -0
  23. data/src/test/java/org/embulk/output/kintone/KintonePageOutputVerifier.java +221 -0
  24. data/src/test/java/org/embulk/output/kintone/OutputPageBuilder.java +92 -0
  25. data/src/test/java/org/embulk/output/kintone/TestKintoneOutputPlugin.java +176 -1
  26. data/src/test/java/org/embulk/output/kintone/TestTask.java +40 -0
  27. data/src/test/java/org/embulk/output/kintone/TestTaskMode.java +43 -0
  28. data/src/test/resources/logback-test.xml +14 -0
  29. data/src/test/resources/org/embulk/output/kintone/config.yml +4 -0
  30. data/src/test/resources/org/embulk/output/kintone/task/config.yml +1 -0
  31. data/src/test/resources/org/embulk/output/kintone/task/mode/config.yml +158 -0
  32. data/src/test/resources/org/embulk/output/kintone/task/mode/input.csv +7 -0
  33. data/src/test/resources/org/embulk/output/kintone/task/mode/insert_add_ignore_nulls_records.jsonl +6 -0
  34. data/src/test/resources/org/embulk/output/kintone/task/mode/insert_add_prefer_nulls_records.jsonl +6 -0
  35. data/src/test/resources/org/embulk/output/kintone/task/mode/insert_add_records.jsonl +6 -0
  36. data/src/test/resources/org/embulk/output/kintone/task/mode/update_update_ignore_nulls_records.jsonl +3 -0
  37. data/src/test/resources/org/embulk/output/kintone/task/mode/update_update_prefer_nulls_records.jsonl +3 -0
  38. data/src/test/resources/org/embulk/output/kintone/task/mode/update_update_records.jsonl +6 -0
  39. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_add_ignore_nulls_records.jsonl +3 -0
  40. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_add_prefer_nulls_records.jsonl +3 -0
  41. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_add_records.jsonl +2 -0
  42. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_update_ignore_nulls_records.jsonl +3 -0
  43. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_update_prefer_nulls_records.jsonl +3 -0
  44. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_update_records.jsonl +4 -0
  45. data/src/test/resources/org/embulk/output/kintone/task/mode/values.json +1 -0
  46. data/src/test/resources/org/embulk/output/kintone/task/mode/values_ignore_nulls.json +1 -0
  47. data/src/test/resources/org/embulk/output/kintone/task/mode/values_prefer_nulls.json +1 -0
  48. metadata +36 -3
  49. 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.concurrent.TimeUnit;
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
- public static final int UPSERT_BATCH_SIZE = 10000;
30
- public static final int CHUNK_SIZE = 100;
31
- private static final Logger LOGGER = LoggerFactory.getLogger(KintonePageOutput.class);
32
- private final PageReader pageReader;
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 (this.client == null) {
67
- return;
90
+ if (client == null) {
91
+ return; // Not connected
68
92
  }
69
93
  try {
70
- this.client.close();
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 interface Consumer<T> {
87
- void accept(T t);
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().orElse(-1));
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
- this.client =
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
- this.client = builder.authByApiToken(task.getToken().get()).build();
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 RuntimeException("Failed to connect to kintone.");
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 insertPage(final Page page) {
117
- execute(
118
- client -> {
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
- records.add(record);
132
- if (records.size() == CHUNK_SIZE) {
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 void updatePage(final Page page) {
148
- execute(
149
- client -> {
150
- try {
151
- ArrayList<RecordForUpdate> updateRecords = new ArrayList<>();
152
- pageReader.setPage(page);
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
- KintoneColumnVisitor visitor =
155
- new KintoneColumnVisitor(
156
- pageReader,
157
- task.getColumnOptions(),
158
- task.getUpdateKeyName()
159
- .orElseThrow(
160
- () -> new RuntimeException("unreachable"))); // Already validated
161
- while (pageReader.nextRecord()) {
162
- Record record = new Record();
163
- UpdateKey updateKey = new UpdateKey();
164
- visitor.setRecord(record);
165
- visitor.setUpdateKey(updateKey);
166
- for (Column column : pageReader.getSchema().getColumns()) {
167
- column.visit(visitor);
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
- if (updateKey.getValue() == "") {
171
- continue;
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
- record.removeField(updateKey.getField());
175
- updateRecords.add(new RecordForUpdate(updateKey, record));
176
- if (updateRecords.size() == CHUNK_SIZE) {
177
- client.record().updateRecords(task.getAppId(), updateRecords);
178
- updateRecords.clear();
179
- sleep();
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 upsertPage(final Page page) {
192
- execute(
193
- client -> {
194
- try {
195
- ArrayList<Record> records = new ArrayList<>();
196
- ArrayList<UpdateKey> updateKeys = new ArrayList<>();
197
- pageReader.setPage(page);
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
- KintoneColumnVisitor visitor =
200
- new KintoneColumnVisitor(
201
- pageReader,
202
- task.getColumnOptions(),
203
- task.getUpdateKeyName()
204
- .orElseThrow(
205
- () -> new RuntimeException("unreachable"))); // Already validated
206
- while (pageReader.nextRecord()) {
207
- Record record = new Record();
208
- UpdateKey updateKey = new UpdateKey();
209
- visitor.setRecord(record);
210
- visitor.setUpdateKey(updateKey);
211
- for (Column column : pageReader.getSchema().getColumns()) {
212
- column.visit(visitor);
213
- }
214
- records.add(record);
215
- updateKeys.add(updateKey);
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
- if (records.size() == UPSERT_BATCH_SIZE) {
218
- upsert(records, updateKeys);
219
- records.clear();
220
- updateKeys.clear();
221
- }
222
- }
223
- if (records.size() > 0) {
224
- upsert(records, updateKeys);
225
- }
226
- } catch (Exception e) {
227
- throw new RuntimeException("kintone throw exception", e);
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(ArrayList<Record> records, ArrayList<UpdateKey> updateKeys)
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
- FieldType updateKeyFieldType =
238
- client.app().getFormFields(task.getAppId()).get(updateKeys.get(0).getField()).getType();
239
- if (!Arrays.asList(FieldType.SINGLE_LINE_TEXT, FieldType.NUMBER).contains(updateKeyFieldType)) {
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
- client.record().addRecords(task.getAppId(), insertRecords);
304
+ insert(insertRecords);
278
305
  insertRecords.clear();
279
- sleep();
280
306
  } else if (updateRecords.size() == CHUNK_SIZE) {
281
- client.record().updateRecords(task.getAppId(), updateRecords);
307
+ update(updateRecords);
282
308
  updateRecords.clear();
283
- sleep();
284
309
  }
285
310
  }
286
- if (insertRecords.size() > 0) {
287
- client.record().addRecords(task.getAppId(), insertRecords);
311
+ if (!insertRecords.isEmpty()) {
312
+ insert(insertRecords);
288
313
  }
289
- if (updateRecords.size() > 0) {
290
- client.record().updateRecords(task.getAppId(), updateRecords);
314
+ if (!updateRecords.isEmpty()) {
315
+ update(updateRecords);
291
316
  }
292
317
  }
293
318
 
294
- private List<Record> getExistingRecordsByUpdateKey(ArrayList<UpdateKey> updateKeys) {
295
- String fieldCode = updateKeys.get(0).getField();
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().toString() + "\"")
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 allRecords;
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> distValues, UpdateKey updateKey) {
326
- return distValues.stream().anyMatch(v -> v.equals(updateKey.getValue().toString()));
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 sleep() throws InterruptedException {
330
- if (!task.getIntervalSeconds().isPresent()) {
331
- return;
332
- }
333
- Integer interval = task.getIntervalSeconds().get();
334
- LOGGER.info(String.format("sleep %d seconds.", interval));
335
- TimeUnit.SECONDS.sleep(interval);
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("interval_seconds")
53
- @ConfigDefault("null")
54
- Optional<Integer> getIntervalSeconds();
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
+ }