2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved.
6 * ================================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 * ============LICENSE_END============================================
19 * ===================================================================
23 export default function CsvToJson(rawCsvData, delimiter, internalDelimiter, csvHeaderNames, jsonKeyNames, mandatory) {
25 let printDictKeys = '';
26 let result = { jsonObjArray: [], errorMessages: '' };
28 // Validate that all parallel arrays passed in have same number of elements;
29 // this would be a developer error.
31 let checkLength = csvHeaderNames.length;
33 if (checkLength !== jsonKeyNames.length || checkLength !== mandatory.length) {
34 result.errorMessages = 'interanl error: csvHeaderNames, jsonKeyNames, and mandatory arrays parameters are not the same length';
38 if (checkLength < 1) {
39 result.errorMessages = 'interanl error: csvHeaderNames, jsonKeyNames, and mandatory arrays have no entries';
43 // Make a nice string to print in the error case to tell user what is the
44 // required heaer row format
46 for (let i=0; i < csvHeaderNames.length; ++i) {
48 printDictKeys = csvHeaderNames[i];
50 printDictKeys += ',' + csvHeaderNames[i];
54 let dictElems = rawCsvData.split('\n');
56 let filteredDictElems = [];
58 // The task of the following loop is to convert raw CSV rows into easily parseable
59 // and streamlined versions of the rows with an internalDelimiter replacing the standard
60 // comma; it is presumed (and checked) that the internalDelimiter cannot exist as a valid
61 // sequence of characters in the user's data.
63 // This conversion process also strips leading and trailing whitespace from each row,
64 // discards empty rows, correctly interprets and removes all double quotes that programs like
65 // Excel use to support user columns that contain special characters, most notably, the comma
66 // delimiter. A double-quote that is contained within a double-quoted column value
67 // must appear in this raw data as a sequence of two double quotes. Furthermore, any column
68 // value in the raw CSV data that does not contain a delimiter may or may not be enclosed in
69 // double quotes. It is the Excel convention to not use double qoutes unless necessary, and
70 // there is no reasonable way to tell Excel to surround every column value with double quotes.
71 // Any files that were directly "exported" by CLAMP itself from the Managing Dictionaries
72 // capability, surround all columns with double quotes.
74 for (let i = 0; i < dictElems.length; i++) {
76 let oneRow = dictElems[i].trim();
79 let nextChar = undefined;
84 continue; // Skip blank rows
85 } else if (oneRow.indexOf(internalDelimiter) !== -1) {
86 result.errorMessages += '\nRow #' + i + ' contains illegal sequence of characters (' + internalDelimiter + ')';
95 // This "while loop" performs the very meticulous task of removing double quotes that
96 // are used by Excel to encase special characters as user string value data,
97 // and manages to correctly identify columns that are defined with or without
98 // double quotes and to process the comma delimiter correctly when encountered
99 // as a user value within a column. Such a column would have to be encased in
100 // double quotes; a comma found outside double quotes IS a delimiter.
102 while (j < oneRow.length) {
103 if (oneRow[j] === '"') {
104 if (inQuote === false) {
105 if (prevChar !== delimiter && prevChar !== null) {
106 result.errorMessages += '\nMismatched double quotes or illegal whitespace around delimiter at row #' + (i + 1) + ' near column #' + numColumns;
112 if (nextChar === '"') {
115 } else if ((nextChar !== delimiter) && (nextChar !== undefined)) {
116 result.errorMessages += '\nRow #' + (i + 1) + ' is badly formatted at column #' + numColumns + '. Perhaps an unescaped double quote.';
118 } else if (nextChar === delimiter) {
121 newStr += internalDelimiter;
122 prevChar = delimiter;
124 nextChar = oneRow[j+1];
133 if (oneRow[j] === delimiter && inQuote === false) {
134 newStr += internalDelimiter;
140 prevChar = oneRow[j];
142 nextChar = oneRow[j+1]; // can result in undefined at the end
145 if (result.errorMessages === '' && inQuote !== false) {
146 result.errorMessages += '\nMismatched double quotes at row #' + (i + 1);
148 } else if (result.errorMessages === '' && numColumns < jsonKeyNames.length) {
149 result.errorMessages += '\nNot enough columns (' + jsonKeyNames.length + ') at row #' + (i + 1);
153 filteredDictElems.push(newStr);
156 if (result.errorMessages !== '') {
160 // Perform further checks on data that is now in JSON form
161 if (filteredDictElems.length < 2) {
162 result.errorMessages += '\nNot enough row data found in import file. Need at least a header row and one row of data';
166 // Now that we have something reliably parsed into sanitized columns lets run some checks
167 // and convert it all into an array of JSON objects to push to the back end if all the
170 let headers = filteredDictElems[0].split(internalDelimiter);
172 // check that headers are included in proper order
173 for (let i=0; i < jsonKeyNames.length; ++i) {
174 if (csvHeaderNames[i] !== headers[i]) {
175 result.errorMessages += 'Row 1 header key at column #' + (i + 1) + ' is a mismatch. Expected row header must contain at least:\n' + printDictKeys;
180 // Convert the ASCII rows of data into an array of JSON obects that omit the header
181 // row which is not sent to the back end.
183 for (let i = 1; i < filteredDictElems.length; i++) {
184 let data = filteredDictElems[i].split(internalDelimiter);
186 for (let j = 0; j < data.length && j < jsonKeyNames.length; j++) {
187 let value = data[j].trim();
188 if (mandatory[j] === true && value === '') {
189 result.errorMessages += '\n' + csvHeaderNames[j] + ' at row #' + (i+1) + ' is empty but requires a value.';
191 obj[jsonKeyNames[j]] = value;
193 result.jsonObjArray.push(obj);
196 if (result.errorMessages !== '') {
197 // If we have errors, return empty parse result even though some things
198 // may have parsed properly. We do not want to encourage the caller
199 // to think the data is good for use.
200 result.jsonObjArray = [];