Skip to content

Commit 4c4cdf4

Browse files
authored
Merge pull request #37 from yas-okadatech/expand-sutable-and-empty-fields
add expand_subtable and handling empty fields
2 parents 5493826 + d840140 commit 4c4cdf4

File tree

7 files changed

+281
-12
lines changed

7 files changed

+281
-12
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ e.g. limit, offset are not supported.
2929
- **basic_auth_username**: Kintone basic auth username Please see Kintone basic auth [here](https://jp.cybozu.help/general/en/admin/list_security/list_ip_basic/basic_auth.html) (string, optional)
3030
- **basic_auth_password**: Kintone basic auth password (string, optional)
3131
- **guest_space_id**: Kintone app belongs to guest space, guest space id is required. (integer, optional)
32-
- **fields** (required)
32+
- **expand_subtable**: Expand subtabble (boolean, default: `false`)
33+
- **fields**: If fields is empty, include all available columns (required)
3334
- **name** the field code of Kintone app record will be retrieved.
3435
- **type** Column values are converted to this embulk type. Available values options are: boolean, long, double, string, json, timestamp) Kintone `SUBTABLE` type is loaded as json text.
3536
- **format** Format of the timestamp if type is timestamp. The format for kintone DATETIME is `%Y-%m-%dT%H:%M:%S%z`.

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ repositories {
1212
}
1313

1414
group = "io.trocco"
15-
version = "0.1.8"
15+
version = "0.1.9"
1616

1717
sourceCompatibility = 1.8
1818
targetCompatibility = 1.8

src/main/java/org/embulk/input/kintone/KintoneClient.java

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,44 @@
11
package org.embulk.input.kintone;
22

3+
import com.google.common.annotations.VisibleForTesting;
4+
import com.kintone.client.AppClient;
35
import com.kintone.client.KintoneClientBuilder;
46
import com.kintone.client.RecordClient;
57
import com.kintone.client.api.record.CreateCursorRequest;
68
import com.kintone.client.api.record.CreateCursorResponseBody;
79
import com.kintone.client.api.record.GetRecordsByCursorResponseBody;
810
import com.kintone.client.exception.KintoneApiRuntimeException;
11+
import com.kintone.client.model.app.field.FieldProperty;
12+
import com.kintone.client.model.app.field.SubtableFieldProperty;
13+
import com.kintone.client.model.record.FieldType;
914
import org.embulk.config.ConfigException;
10-
import org.embulk.util.config.units.ColumnConfig;
15+
import org.embulk.spi.Column;
16+
import org.embulk.spi.Schema;
1117
import org.slf4j.Logger;
1218
import org.slf4j.LoggerFactory;
1319

1420
import java.util.ArrayList;
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.Map;
1524

1625
public class KintoneClient
1726
{
1827
private final Logger logger = LoggerFactory.getLogger(KintoneClient.class);
1928
private static final int FETCH_SIZE = 500;
2029
private RecordClient recordClient;
30+
private AppClient appClient;
2131

2232
public KintoneClient() throws ConfigException
2333
{
2434
}
2535

36+
@VisibleForTesting
37+
protected KintoneClient(AppClient appClient)
38+
{
39+
this.appClient = appClient;
40+
}
41+
2642
@SuppressWarnings("StatementWithEmptyBody")
2743
public void validateAuth(final PluginTask task) throws ConfigException
2844
{
@@ -57,11 +73,12 @@ else if (task.getToken().isPresent()) {
5773

5874
com.kintone.client.KintoneClient client = builder.build();
5975
this.recordClient = client.record();
76+
this.appClient = client.app();
6077
}
6178

62-
public GetRecordsByCursorResponseBody getResponse(final PluginTask task)
79+
public GetRecordsByCursorResponseBody getResponse(final PluginTask task, final Schema schema)
6380
{
64-
CreateCursorResponseBody cursor = this.createCursor(task);
81+
CreateCursorResponseBody cursor = this.createCursor(task, schema);
6582
try {
6683
return this.recordClient.getRecordsByCursor(cursor.getId());
6784
}
@@ -84,12 +101,16 @@ public GetRecordsByCursorResponseBody getRecordsByCursor(String cursor)
84101
}
85102
}
86103

87-
public CreateCursorResponseBody createCursor(final PluginTask task)
104+
public CreateCursorResponseBody createCursor(final PluginTask task, final Schema schema)
88105
{
89106
ArrayList<String> fields = new ArrayList<>();
90-
for (ColumnConfig c : task.getFields().getColumns()) {
107+
for (Column c : schema.getColumns()) {
91108
fields.add(c.getName());
92109
}
110+
if (task.getExpandSubtable()) {
111+
List<String> subTableFieldCodes = getFieldCodes(task, FieldType.SUBTABLE);
112+
fields.addAll(subTableFieldCodes);
113+
}
93114
CreateCursorRequest request = new CreateCursorRequest();
94115
request.setApp((long) task.getAppId());
95116
request.setFields(fields);
@@ -113,4 +134,37 @@ public void deleteCursor(String cursor)
113134
this.logger.error(e.toString());
114135
}
115136
}
137+
138+
public Map<String, FieldProperty> getFields(final PluginTask task)
139+
{
140+
Map<String, FieldProperty> fields = this.appClient.getFormFields(task.getAppId());
141+
if (task.getExpandSubtable()) {
142+
Map<String, FieldProperty> subtableFields = new HashMap<>();
143+
List<String> subtableFieldCodes = new ArrayList<>();
144+
for (Map.Entry<String, FieldProperty> fieldEntry : fields.entrySet()) {
145+
if (fieldEntry.getValue().getType() == FieldType.SUBTABLE) {
146+
subtableFields.putAll(((SubtableFieldProperty) fieldEntry.getValue()).getFields());
147+
subtableFieldCodes.add(fieldEntry.getKey());
148+
}
149+
}
150+
for (String subtableFieldCode : subtableFieldCodes) {
151+
fields.remove(subtableFieldCode);
152+
}
153+
fields.putAll(subtableFields);
154+
}
155+
156+
return fields;
157+
}
158+
159+
public List<String> getFieldCodes(final PluginTask task, FieldType fieldType)
160+
{
161+
ArrayList<String> fieldCodes = new ArrayList<>();
162+
Map<String, FieldProperty> fields = this.appClient.getFormFields(task.getAppId());
163+
for (Map.Entry<String, FieldProperty> entry : fields.entrySet()) {
164+
if (entry.getValue().getType() == fieldType) {
165+
fieldCodes.add(entry.getKey());
166+
}
167+
}
168+
return fieldCodes;
169+
}
116170
}

src/main/java/org/embulk/input/kintone/KintoneInputPlugin.java

Lines changed: 124 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import com.google.common.annotations.VisibleForTesting;
44
import com.kintone.client.api.record.CreateCursorResponseBody;
55
import com.kintone.client.api.record.GetRecordsByCursorResponseBody;
6+
import com.kintone.client.model.app.field.FieldProperty;
7+
import com.kintone.client.model.record.FieldType;
68
import com.kintone.client.model.record.Record;
9+
import com.kintone.client.model.record.TableRow;
710
import org.embulk.config.ConfigDiff;
811
import org.embulk.config.ConfigSource;
912
import org.embulk.config.TaskReport;
@@ -13,14 +16,21 @@
1316
import org.embulk.spi.PageBuilder;
1417
import org.embulk.spi.PageOutput;
1518
import org.embulk.spi.Schema;
19+
import org.embulk.spi.Schema.Builder;
20+
import org.embulk.spi.type.Type;
21+
import org.embulk.spi.type.Types;
1622
import org.embulk.util.config.ConfigMapper;
1723
import org.embulk.util.config.ConfigMapperFactory;
1824
import org.embulk.util.config.TaskMapper;
1925
import org.embulk.util.config.modules.TimestampModule;
2026
import org.slf4j.Logger;
2127
import org.slf4j.LoggerFactory;
2228

29+
import java.util.ArrayList;
2330
import java.util.List;
31+
import java.util.Map;
32+
import java.util.Set;
33+
import java.util.TreeMap;
2434

2535
public class KintoneInputPlugin
2636
implements InputPlugin
@@ -41,6 +51,10 @@ public ConfigDiff transaction(ConfigSource config,
4151
Schema schema = task.getFields().toSchema();
4252
int taskCount = 1; // number of run() method calls
4353

54+
if (schema.isEmpty()) {
55+
schema = buildSchema(task);
56+
}
57+
4458
return resume(task.toTaskSource(), schema, taskCount, control);
4559
}
4660

@@ -74,14 +88,30 @@ public TaskReport run(TaskSource taskSource,
7488
client.validateAuth(task);
7589
client.connect(task);
7690

77-
CreateCursorResponseBody cursor = client.createCursor(task);
91+
CreateCursorResponseBody cursor = client.createCursor(task, schema);
7892
GetRecordsByCursorResponseBody cursorResponse = new GetRecordsByCursorResponseBody(true, null);
7993

94+
List<String> subTableFieldCodes = null;
95+
if (task.getExpandSubtable()) {
96+
subTableFieldCodes = client.getFieldCodes(task, FieldType.SUBTABLE);
97+
}
98+
8099
while (cursorResponse.isNext()) {
81100
cursorResponse = client.getRecordsByCursor(cursor.getId());
82101
for (Record record : cursorResponse.getRecords()) {
83-
schema.visitColumns(new KintoneInputColumnVisitor(new KintoneAccessor(record), pageBuilder, task));
84-
pageBuilder.addRecord();
102+
List<Record> records;
103+
if (task.getExpandSubtable()) {
104+
records = expandSubtable(record, subTableFieldCodes);
105+
}
106+
else {
107+
records = new ArrayList<>();
108+
records.add(record);
109+
}
110+
111+
for (Record expandedRecord : records) {
112+
schema.visitColumns(new KintoneInputColumnVisitor(new KintoneAccessor(expandedRecord), pageBuilder, task));
113+
pageBuilder.addRecord();
114+
}
85115
}
86116
pageBuilder.flush();
87117
}
@@ -113,4 +143,95 @@ protected KintoneClient getKintoneClient()
113143
{
114144
return new KintoneClient();
115145
}
146+
147+
private List<Record> expandSubtable(final Record originalRecord, final List<String> subTableFieldCodes)
148+
{
149+
ArrayList<Record> records = new ArrayList<>();
150+
records.add(cloneRecord(originalRecord));
151+
for (String fieldCode : subTableFieldCodes) {
152+
List<TableRow> tableRows = originalRecord.getSubtableFieldValue(fieldCode);
153+
for (int idx = 0; idx < tableRows.size(); idx++) {
154+
if (records.size() < idx + 1) {
155+
records.add(cloneRecord(originalRecord));
156+
}
157+
158+
TableRow tableRow = tableRows.get(idx);
159+
Record currentRecord = records.get(idx);
160+
Set<String> tableFieldCodes = tableRow.getFieldCodes();
161+
for (String tableFieldCode : tableFieldCodes) {
162+
currentRecord.putField(tableFieldCode, tableRow.getFieldValue(tableFieldCode));
163+
}
164+
}
165+
}
166+
return records;
167+
}
168+
169+
private Record cloneRecord(final Record src)
170+
{
171+
Record dst = new Record(src.getId(), src.getRevision());
172+
for (String fieldCode : src.getFieldCodes(true)) {
173+
dst.putField(fieldCode, src.getFieldValue(fieldCode));
174+
}
175+
return dst;
176+
}
177+
178+
private Schema buildSchema(final PluginTask task)
179+
{
180+
KintoneClient client = getKintoneClient();
181+
client.validateAuth(task);
182+
client.connect(task);
183+
184+
Map<String, FieldProperty> fields = new TreeMap<>(client.getFields(task));
185+
Builder builder = Schema.builder();
186+
187+
// built in schema
188+
builder.add("$id", Types.LONG);
189+
builder.add("$revision", Types.LONG);
190+
191+
for (Map.Entry<String, FieldProperty> fieldEntry : fields.entrySet()) {
192+
builder.add(fieldEntry.getKey(), buildType(fieldEntry.getValue().getType()));
193+
}
194+
195+
return builder.build();
196+
}
197+
198+
private Type buildType(final FieldType fieldType)
199+
{
200+
switch(fieldType) {
201+
case __ID__:
202+
case __REVISION__:
203+
case RECORD_NUMBER:
204+
return Types.LONG;
205+
case CALC:
206+
case NUMBER:
207+
return Types.DOUBLE;
208+
case CREATED_TIME:
209+
case DATETIME:
210+
case UPDATED_TIME:
211+
return Types.TIMESTAMP;
212+
case SUBTABLE:
213+
return Types.JSON;
214+
case CATEGORY:
215+
case CHECK_BOX:
216+
case CREATOR:
217+
case DATE:
218+
case DROP_DOWN:
219+
case FILE:
220+
case GROUP_SELECT:
221+
case LINK:
222+
case MODIFIER:
223+
case MULTI_LINE_TEXT:
224+
case MULTI_SELECT:
225+
case ORGANIZATION_SELECT:
226+
case RADIO_BUTTON:
227+
case RICH_TEXT:
228+
case SINGLE_LINE_TEXT:
229+
case STATUS:
230+
case STATUS_ASSIGNEE:
231+
case TIME:
232+
case USER_SELECT:
233+
default:
234+
return Types.STRING;
235+
}
236+
}
116237
}

src/main/java/org/embulk/input/kintone/PluginTask.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ public interface PluginTask
4444
@ConfigDefault("null")
4545
Optional<String> getQuery();
4646

47+
@Config("expand_subtable")
48+
@ConfigDefault("false")
49+
boolean getExpandSubtable();
50+
4751
@Config("fields")
4852
SchemaConfig getFields();
4953
}

0 commit comments

Comments
 (0)