diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java index 925abf88..4a4b2442 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java @@ -35,6 +35,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.AbstractQFieldMapping; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -133,11 +134,17 @@ public class CsvToQRecordAdapter CSVFormat.DEFAULT .withFirstRecordAsHeader() .withIgnoreHeaderCase() + .withIgnoreEmptyLines() .withTrim()); List headers = csvParser.getHeaderNames(); headers = makeHeadersUnique(headers); + //////////////////////////////////////// + // used by csv-headers-as-field-names // + //////////////////////////////////////// + Map csvHeaderFieldMapping = buildCsvHeaderFieldMappingIfNeeded(inputWrapper, headers); + Iterator csvIterator = csvParser.iterator(); int recordCount = 0; while(csvIterator.hasNext()) @@ -160,11 +167,27 @@ public class CsvToQRecordAdapter QRecord qRecord = new QRecord(); try { - for(QFieldMetaData field : table.getFields().values()) + if(inputWrapper.getCsvHeadersAsFieldNames()) { - String fieldSource = mapping == null ? field.getName() : String.valueOf(mapping.getFieldSource(field.getName())); - fieldSource = adjustHeaderCase(fieldSource, inputWrapper); - setValue(inputWrapper, qRecord, field, csvValues.get(fieldSource)); + ///////////////////////////////////////////////////////////////////////////////////////// + // in csv-headers-as-field-names mode, don't mess with table, and don't do any mapping // + ///////////////////////////////////////////////////////////////////////////////////////// + for(Map.Entry entry : csvValues.entrySet()) + { + setValue(inputWrapper, qRecord, csvHeaderFieldMapping.get(entry.getKey()), entry.getValue()); + } + } + else + { + /////////////////////////////////////// + // otherwise, fields come from table // + /////////////////////////////////////// + for(QFieldMetaData field : table.getFields().values()) + { + String fieldSource = mapping == null ? field.getName() : String.valueOf(mapping.getFieldSource(field.getName())); + fieldSource = adjustHeaderCase(fieldSource, inputWrapper); + setValue(inputWrapper, qRecord, field, csvValues.get(fieldSource)); + } } runRecordCustomizer(recordCustomizer, qRecord); @@ -247,6 +270,26 @@ public class CsvToQRecordAdapter + /******************************************************************************* + ** + *******************************************************************************/ + private Map buildCsvHeaderFieldMappingIfNeeded(InputWrapper inputWrapper, List headers) + { + Map csvHeaderFieldMapping = null; + if(inputWrapper.getCsvHeadersAsFieldNames()) + { + csvHeaderFieldMapping = new HashMap<>(); + for(String header : headers) + { + header = adjustHeaderCase(header, inputWrapper); + csvHeaderFieldMapping.put(header, new QFieldMetaData(header, QFieldType.STRING)); + } + } + return csvHeaderFieldMapping; + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -376,7 +419,8 @@ public class CsvToQRecordAdapter private Integer limit; private boolean doCorrectValueTypes = false; - private boolean caseSensitiveHeaders = false; + private boolean caseSensitiveHeaders = false; + private boolean csvHeadersAsFieldNames = false; @@ -618,6 +662,40 @@ public class CsvToQRecordAdapter + /******************************************************************************* + ** Getter for csvHeadersAsFieldNames + ** + *******************************************************************************/ + public boolean getCsvHeadersAsFieldNames() + { + return csvHeadersAsFieldNames; + } + + + + /******************************************************************************* + ** Setter for csvHeadersAsFieldNames + ** + *******************************************************************************/ + public void setCsvHeadersAsFieldNames(boolean csvHeadersAsFieldNames) + { + this.csvHeadersAsFieldNames = csvHeadersAsFieldNames; + } + + + + /******************************************************************************* + ** Fluent setter for csvHeadersAsFieldNames + ** + *******************************************************************************/ + public InputWrapper withCsvHeadersAsFieldNames(boolean csvHeadersAsFieldNames) + { + this.csvHeadersAsFieldNames = csvHeadersAsFieldNames; + return (this); + } + + + /******************************************************************************* ** Getter for doCorrectValueTypes ** diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java index cb71a4fe..66d6e163 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapterTest.java @@ -465,4 +465,60 @@ class CsvToQRecordAdapterTest extends BaseTest assertThat(qRecord.getErrors().get(0).toString()).isEqualTo("Error parsing line #2: Value [green] could not be converted to an Integer."); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCsvHeadersAsFields() throws QException + { + CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); + csvToQRecordAdapter.buildRecordsFromCsv(new CsvToQRecordAdapter.InputWrapper() + .withCsvHeadersAsFieldNames(true) + .withCaseSensitiveHeaders(true) + .withCsv(""" + firstName,birthDate,favoriteShapeId + John,1980,1 + Paul,1970-06-15,green + """)); + + List qRecords = csvToQRecordAdapter.getRecordList(); + + QRecord qRecord = qRecords.get(0); + assertEquals("John", qRecord.getValue("firstName")); + assertEquals("1980", qRecord.getValue("birthDate")); + assertEquals("1", qRecord.getValue("favoriteShapeId")); + + qRecord = qRecords.get(1); + assertEquals("Paul", qRecord.getValue("firstName")); + assertEquals("1970-06-15", qRecord.getValue("birthDate")); + assertEquals("green", qRecord.getValue("favoriteShapeId")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCsvHeadersAsFieldsDuplicatedNames() throws QException + { + CsvToQRecordAdapter csvToQRecordAdapter = new CsvToQRecordAdapter(); + csvToQRecordAdapter.buildRecordsFromCsv(new CsvToQRecordAdapter.InputWrapper() + .withCsvHeadersAsFieldNames(true) + .withCaseSensitiveHeaders(true) + .withCsv(""" + orderId,sku,sku + 10001,BASIC1,BASIC2 + """)); + + List qRecords = csvToQRecordAdapter.getRecordList(); + + QRecord qRecord = qRecords.get(0); + assertEquals("10001", qRecord.getValue("orderId")); + assertEquals("BASIC1", qRecord.getValue("sku")); + assertEquals("BASIC2", qRecord.getValue("sku 2")); + } + }