embulk-output-kintone 0.4.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }