embulk-output-kintone 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,6 +7,14 @@ import com.kintone.client.model.record.FieldType;
7
7
  import com.kintone.client.model.record.Record;
8
8
  import com.kintone.client.model.record.RecordForUpdate;
9
9
  import com.kintone.client.model.record.UpdateKey;
10
+ import java.math.BigDecimal;
11
+ import java.util.ArrayList;
12
+ import java.util.Arrays;
13
+ import java.util.Collections;
14
+ import java.util.List;
15
+ import java.util.concurrent.TimeUnit;
16
+ import java.util.stream.Collectors;
17
+ import org.embulk.config.ConfigException;
10
18
  import org.embulk.config.TaskReport;
11
19
  import org.embulk.spi.Column;
12
20
  import org.embulk.spi.Exec;
@@ -14,313 +22,316 @@ import org.embulk.spi.Page;
14
22
  import org.embulk.spi.PageReader;
15
23
  import org.embulk.spi.Schema;
16
24
  import org.embulk.spi.TransactionalPageOutput;
17
-
18
- import java.util.ArrayList;
19
- import java.util.Collections;
20
- import java.util.List;
21
- import java.util.stream.Collectors;
22
-
23
- public class KintonePageOutput
24
- implements TransactionalPageOutput
25
- {
26
- public static final int UPSERT_BATCH_SIZE = 10000;
27
- public static final int CHUNK_SIZE = 100;
28
- private final PageReader pageReader;
29
- private final PluginTask task;
30
- private KintoneClient client;
31
-
32
- public KintonePageOutput(PluginTask task, Schema schema)
33
- {
34
- this.pageReader = new PageReader(schema);
35
- this.task = task;
25
+ import org.slf4j.Logger;
26
+ import org.slf4j.LoggerFactory;
27
+
28
+ 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;
33
+ private final PluginTask task;
34
+ private KintoneClient client;
35
+
36
+ public KintonePageOutput(PluginTask task, Schema schema) {
37
+ this.pageReader = new PageReader(schema);
38
+ this.task = task;
39
+ }
40
+
41
+ @Override
42
+ public void add(Page page) {
43
+ KintoneMode mode = KintoneMode.getKintoneModeByValue(task.getMode());
44
+ switch (mode) {
45
+ case INSERT:
46
+ insertPage(page);
47
+ break;
48
+ case UPDATE:
49
+ updatePage(page);
50
+ break;
51
+ case UPSERT:
52
+ upsertPage(page);
53
+ break;
54
+ default:
55
+ throw new UnsupportedOperationException(String.format("Unknown mode '%s'", task.getMode()));
36
56
  }
57
+ }
37
58
 
38
- @Override
39
- public void add(Page page)
40
- {
41
- KintoneMode mode = KintoneMode.getKintoneModeByValue(task.getMode());
42
- switch (mode) {
43
- case INSERT:
44
- insertPage(page);
45
- break;
46
- case UPDATE:
47
- updatePage(page);
48
- break;
49
- case UPSERT:
50
- upsertPage(page);
51
- break;
52
- default:
53
- throw new UnsupportedOperationException(String.format(
54
- "Unknown mode '%s'",
55
- task.getMode()));
56
- }
57
- }
59
+ @Override
60
+ public void finish() {
61
+ // noop
62
+ }
58
63
 
59
- @Override
60
- public void finish()
61
- {
62
- // noop
64
+ @Override
65
+ public void close() {
66
+ if (this.client == null) {
67
+ return;
63
68
  }
64
-
65
- @Override
66
- public void close()
67
- {
68
- if (this.client == null) {
69
- return;
70
- }
71
- try {
72
- this.client.close();
73
- }
74
- catch (Exception e) {
75
- throw new RuntimeException("kintone throw exception", e);
76
- }
69
+ try {
70
+ this.client.close();
71
+ } catch (Exception e) {
72
+ throw new RuntimeException("kintone throw exception", e);
77
73
  }
78
-
79
- @Override
80
- public void abort()
81
- {
82
- // noop
74
+ }
75
+
76
+ @Override
77
+ public void abort() {
78
+ // noop
79
+ }
80
+
81
+ @Override
82
+ public TaskReport commit() {
83
+ return Exec.newTaskReport();
84
+ }
85
+
86
+ public interface Consumer<T> {
87
+ void accept(T t);
88
+ }
89
+
90
+ public void connect(final PluginTask task) {
91
+ KintoneClientBuilder builder = KintoneClientBuilder.create("https://" + task.getDomain());
92
+ if (task.getGuestSpaceId().isPresent()) {
93
+ builder.setGuestSpaceId(task.getGuestSpaceId().orElse(-1));
83
94
  }
84
-
85
- @Override
86
- public TaskReport commit()
87
- {
88
- return Exec.newTaskReport();
95
+ if (task.getBasicAuthUsername().isPresent() && task.getBasicAuthPassword().isPresent()) {
96
+ builder.withBasicAuth(task.getBasicAuthUsername().get(), task.getBasicAuthPassword().get());
89
97
  }
90
98
 
91
- public interface Consumer<T>
92
- {
93
- void accept(T t);
99
+ if (task.getUsername().isPresent() && task.getPassword().isPresent()) {
100
+ this.client =
101
+ builder.authByPassword(task.getUsername().get(), task.getPassword().get()).build();
102
+ } else if (task.getToken().isPresent()) {
103
+ this.client = builder.authByApiToken(task.getToken().get()).build();
94
104
  }
95
-
96
- public void connect(final PluginTask task)
97
- {
98
- KintoneClientBuilder builder = KintoneClientBuilder.create("https://" + task.getDomain());
99
- if (task.getGuestSpaceId().isPresent()) {
100
- builder.setGuestSpaceId(task.getGuestSpaceId().orElse(-1));
101
- }
102
- if (task.getBasicAuthUsername().isPresent() && task.getBasicAuthPassword().isPresent()) {
103
- builder.withBasicAuth(task.getBasicAuthUsername().get(),
104
- task.getBasicAuthPassword().get());
105
- }
106
-
107
- if (task.getUsername().isPresent() && task.getPassword().isPresent()) {
108
- this.client = builder
109
- .authByPassword(task.getUsername().get(), task.getPassword().get())
110
- .build();
111
- }
112
- else if (task.getToken().isPresent()) {
113
- this.client = builder
114
- .authByApiToken(task.getToken().get())
115
- .build();
116
- }
105
+ }
106
+
107
+ private void execute(Consumer<KintoneClient> operation) {
108
+ connect(task);
109
+ if (this.client != null) {
110
+ operation.accept(this.client);
111
+ } else {
112
+ throw new RuntimeException("Failed to connect to kintone.");
117
113
  }
118
-
119
- private void execute(Consumer<KintoneClient> operation)
120
- {
121
- connect(task);
122
- if (this.client != null) {
123
- operation.accept(this.client);
124
- } else {
125
- throw new RuntimeException("Failed to connect to kintone.");
126
- }
127
- }
128
-
129
- private void insertPage(final Page page)
130
- {
131
- execute(client -> {
132
- try {
133
- ArrayList<Record> records = new ArrayList<>();
134
- pageReader.setPage(page);
135
- KintoneColumnVisitor visitor = new KintoneColumnVisitor(pageReader,
136
- task.getColumnOptions());
137
- while (pageReader.nextRecord()) {
138
- Record record = new Record();
139
- visitor.setRecord(record);
140
- for (Column column : pageReader.getSchema().getColumns()) {
141
- column.visit(visitor);
142
- }
143
-
144
- records.add(record);
145
- if (records.size() == CHUNK_SIZE) {
146
- client.record().addRecords(task.getAppId(), records);
147
- records.clear();
148
- }
149
- }
150
- if (records.size() > 0) {
151
- client.record().addRecords(task.getAppId(), records);
152
- }
114
+ }
115
+
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
+ }
130
+
131
+ records.add(record);
132
+ if (records.size() == CHUNK_SIZE) {
133
+ client.record().addRecords(task.getAppId(), records);
134
+ records.clear();
135
+ sleep();
136
+ }
153
137
  }
154
- catch (Exception e) {
155
- throw new RuntimeException("kintone throw exception", e);
138
+ if (records.size() > 0) {
139
+ client.record().addRecords(task.getAppId(), records);
156
140
  }
141
+ } catch (Exception e) {
142
+ throw new RuntimeException("kintone throw exception", e);
143
+ }
157
144
  });
158
- }
159
-
160
- private void updatePage(final Page page)
161
- {
162
- execute(client -> {
163
- try {
164
- ArrayList<RecordForUpdate> updateRecords = new ArrayList<>();
165
- pageReader.setPage(page);
166
-
167
- KintoneColumnVisitor visitor = new KintoneColumnVisitor(pageReader,
168
- task.getColumnOptions(),
169
- task.getUpdateKeyName().orElseThrow(() -> new RuntimeException("unreachable"))); // Already validated
170
- while (pageReader.nextRecord()) {
171
- Record record = new Record();
172
- UpdateKey updateKey = new UpdateKey();
173
- visitor.setRecord(record);
174
- visitor.setUpdateKey(updateKey);
175
- for (Column column : pageReader.getSchema().getColumns()) {
176
- column.visit(visitor);
177
- }
178
-
179
- if (updateKey.getValue() == "") {
180
- continue;
181
- }
182
-
183
- record.removeField(updateKey.getField());
184
- updateRecords.add(new RecordForUpdate(updateKey, record));
185
- if (updateRecords.size() == CHUNK_SIZE) {
186
- client.record().updateRecords(task.getAppId(), updateRecords);
187
- updateRecords.clear();
188
- }
189
- }
190
- if (updateRecords.size() > 0) {
191
- client.record().updateRecords(task.getAppId(), updateRecords);
192
- }
145
+ }
146
+
147
+ private void updatePage(final Page page) {
148
+ execute(
149
+ client -> {
150
+ try {
151
+ ArrayList<RecordForUpdate> updateRecords = new ArrayList<>();
152
+ pageReader.setPage(page);
153
+
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
+ }
169
+
170
+ if (updateKey.getValue() == "") {
171
+ continue;
172
+ }
173
+
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
+ }
193
181
  }
194
- catch (Exception e) {
195
- throw new RuntimeException("kintone throw exception", e);
182
+ if (updateRecords.size() > 0) {
183
+ client.record().updateRecords(task.getAppId(), updateRecords);
196
184
  }
185
+ } catch (Exception e) {
186
+ throw new RuntimeException("kintone throw exception", e);
187
+ }
197
188
  });
198
- }
199
-
200
- private void upsertPage(final Page page)
201
- {
202
- execute(client -> {
203
- try {
204
- ArrayList<Record> records = new ArrayList<>();
205
- ArrayList<UpdateKey> updateKeys = new ArrayList<>();
206
- pageReader.setPage(page);
207
-
208
- KintoneColumnVisitor visitor = new KintoneColumnVisitor(pageReader,
209
- task.getColumnOptions(),
210
- task.getUpdateKeyName().orElseThrow(() -> new RuntimeException("unreachable"))); // Already validated
211
- while (pageReader.nextRecord()) {
212
- Record record = new Record();
213
- UpdateKey updateKey = new UpdateKey();
214
- visitor.setRecord(record);
215
- visitor.setUpdateKey(updateKey);
216
- for (Column column : pageReader.getSchema().getColumns()) {
217
- column.visit(visitor);
218
- }
219
- records.add(record);
220
- updateKeys.add(updateKey);
221
-
222
- if (records.size() == UPSERT_BATCH_SIZE) {
223
- upsert(records, updateKeys);
224
- records.clear();
225
- updateKeys.clear();
226
- }
227
- }
228
- if (records.size() > 0) {
229
- upsert(records, updateKeys);
230
- }
189
+ }
190
+
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);
198
+
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);
216
+
217
+ if (records.size() == UPSERT_BATCH_SIZE) {
218
+ upsert(records, updateKeys);
219
+ records.clear();
220
+ updateKeys.clear();
221
+ }
231
222
  }
232
- catch (Exception e) {
233
- throw new RuntimeException("kintone throw exception", e);
223
+ if (records.size() > 0) {
224
+ upsert(records, updateKeys);
234
225
  }
226
+ } catch (Exception e) {
227
+ throw new RuntimeException("kintone throw exception", e);
228
+ }
235
229
  });
236
- }
237
-
238
- private void upsert(ArrayList<Record> records, ArrayList<UpdateKey> updateKeys)
239
- {
240
- if (records.size() != updateKeys.size()) {
241
- throw new RuntimeException("records.size() != updateKeys.size()");
242
- }
243
-
244
- List<Record> existingRecords = getExistingRecordsByUpdateKey(updateKeys);
245
-
246
- ArrayList<Record> insertRecords = new ArrayList<>();
247
- ArrayList<RecordForUpdate> updateRecords = new ArrayList<>();
248
- for (int i = 0; i < records.size(); i++) {
249
- Record record = records.get(i);
250
- UpdateKey updateKey = updateKeys.get(i);
251
-
252
- if (existsRecord(existingRecords, updateKey)) {
253
- record.removeField(updateKey.getField());
254
- updateRecords.add(new RecordForUpdate(updateKey, record));
255
- }
256
- else {
257
- insertRecords.add(record);
258
- }
230
+ }
259
231
 
260
- if (insertRecords.size() == CHUNK_SIZE) {
261
- client.record().addRecords(task.getAppId(), insertRecords);
262
- insertRecords.clear();
263
- }
264
- else if (updateRecords.size() == CHUNK_SIZE) {
265
- client.record().updateRecords(task.getAppId(), updateRecords);
266
- updateRecords.clear();
267
- }
268
- }
269
- if (insertRecords.size() > 0) {
270
- client.record().addRecords(task.getAppId(), insertRecords);
271
- }
272
- if (updateRecords.size() > 0) {
273
- client.record().updateRecords(task.getAppId(), updateRecords);
274
- }
232
+ private void upsert(ArrayList<Record> records, ArrayList<UpdateKey> updateKeys)
233
+ throws InterruptedException {
234
+ if (records.size() != updateKeys.size()) {
235
+ throw new RuntimeException("records.size() != updateKeys.size()");
236
+ }
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'.");
275
241
  }
276
242
 
277
-
278
- private List<Record> getExistingRecordsByUpdateKey(ArrayList<UpdateKey> updateKeys)
279
- {
280
- String fieldCode = updateKeys.get(0).getField();
281
- List<String> queryValues = updateKeys
282
- .stream()
283
- .filter(k -> k.getValue() != "")
284
- .map(k -> "\"" + k.getValue().toString() + "\"")
285
- .collect(Collectors.toList());
286
-
287
- List<Record> allRecords = new ArrayList<>();
288
- if (queryValues.isEmpty()) {
289
- return allRecords;
290
- }
291
- String cursorId = client.record().createCursor(
292
- task.getAppId(),
293
- Collections.singletonList(fieldCode),
294
- fieldCode + " in (" + String.join(",", queryValues) + ")"
295
- );
296
- while (true) {
297
- GetRecordsByCursorResponseBody resp = client.record().getRecordsByCursor(cursorId);
298
- List<Record> records = resp.getRecords();
299
- allRecords.addAll(records);
300
-
301
- if (!resp.hasNext()) {
302
- break;
303
- }
304
- }
305
- return allRecords;
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<>();
265
+ for (int i = 0; i < records.size(); i++) {
266
+ Record record = records.get(i);
267
+ UpdateKey updateKey = updateKeys.get(i);
268
+
269
+ if (existsRecord(existingValues, updateKey)) {
270
+ record.removeField(updateKey.getField());
271
+ updateRecords.add(new RecordForUpdate(updateKey, record));
272
+ } else {
273
+ insertRecords.add(record);
274
+ }
275
+
276
+ if (insertRecords.size() == CHUNK_SIZE) {
277
+ client.record().addRecords(task.getAppId(), insertRecords);
278
+ insertRecords.clear();
279
+ sleep();
280
+ } else if (updateRecords.size() == CHUNK_SIZE) {
281
+ client.record().updateRecords(task.getAppId(), updateRecords);
282
+ updateRecords.clear();
283
+ sleep();
284
+ }
285
+ }
286
+ if (insertRecords.size() > 0) {
287
+ client.record().addRecords(task.getAppId(), insertRecords);
288
+ }
289
+ if (updateRecords.size() > 0) {
290
+ client.record().updateRecords(task.getAppId(), updateRecords);
306
291
  }
292
+ }
293
+
294
+ private List<Record> getExistingRecordsByUpdateKey(ArrayList<UpdateKey> updateKeys) {
295
+ String fieldCode = updateKeys.get(0).getField();
296
+ List<String> queryValues =
297
+ updateKeys.stream()
298
+ .filter(k -> k.getValue() != "")
299
+ .map(k -> "\"" + k.getValue().toString() + "\"")
300
+ .collect(Collectors.toList());
301
+
302
+ List<Record> allRecords = new ArrayList<>();
303
+ if (queryValues.isEmpty()) {
304
+ return allRecords;
305
+ }
306
+ String cursorId =
307
+ client
308
+ .record()
309
+ .createCursor(
310
+ task.getAppId(),
311
+ Collections.singletonList(fieldCode),
312
+ fieldCode + " in (" + String.join(",", queryValues) + ")");
313
+ while (true) {
314
+ GetRecordsByCursorResponseBody resp = client.record().getRecordsByCursor(cursorId);
315
+ List<Record> records = resp.getRecords();
316
+ allRecords.addAll(records);
317
+
318
+ if (!resp.hasNext()) {
319
+ break;
320
+ }
321
+ }
322
+ return allRecords;
323
+ }
324
+
325
+ private boolean existsRecord(List<String> distValues, UpdateKey updateKey) {
326
+ return distValues.stream().anyMatch(v -> v.equals(updateKey.getValue().toString()));
327
+ }
307
328
 
308
- private boolean existsRecord(List<Record> distRecords, UpdateKey updateKey)
309
- {
310
- String fieldCode = updateKey.getField();
311
- FieldType type = client.app().getFormFields(task.getAppId()).get(fieldCode).getType();
312
- switch (type) {
313
- case SINGLE_LINE_TEXT:
314
- return distRecords
315
- .stream()
316
- .anyMatch(d -> d.getSingleLineTextFieldValue(fieldCode).equals(updateKey.getValue().toString()));
317
- case NUMBER:
318
- return distRecords
319
- .stream()
320
- .anyMatch(d -> d.getNumberFieldValue(fieldCode).toPlainString()
321
- .equals(updateKey.getValue().toString()));
322
- default:
323
- throw new RuntimeException("The update_key must be 'SINGLE_LINE_TEXT' or 'NUMBER'.");
324
- }
329
+ private void sleep() throws InterruptedException {
330
+ if (!task.getIntervalSeconds().isPresent()) {
331
+ return;
325
332
  }
333
+ Integer interval = task.getIntervalSeconds().get();
334
+ LOGGER.info(String.format("sleep %d seconds.", interval));
335
+ TimeUnit.SECONDS.sleep(interval);
336
+ }
326
337
  }