brunomilitzer | 87111ee | 2021-05-18 12:50:32 +0100 | [diff] [blame^] | 1 | /*- |
| 2 | * ============LICENSE_START======================================================= |
| 3 | * ONAP CLAMP |
| 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 |
| 10 | * |
| 11 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 12 | * |
| 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 | * =================================================================== |
| 20 | * |
| 21 | */ |
| 22 | |
| 23 | export default function CsvToJson(rawCsvData, delimiter, internalDelimiter, csvHeaderNames, jsonKeyNames, mandatory) { |
| 24 | |
| 25 | let printDictKeys = ''; |
| 26 | let result = { jsonObjArray: [], errorMessages: '' }; |
| 27 | |
| 28 | // Validate that all parallel arrays passed in have same number of elements; |
| 29 | // this would be a developer error. |
| 30 | |
| 31 | let checkLength = csvHeaderNames.length; |
| 32 | |
| 33 | if (checkLength !== jsonKeyNames.length || checkLength !== mandatory.length) { |
| 34 | result.errorMessages = 'interanl error: csvHeaderNames, jsonKeyNames, and mandatory arrays parameters are not the same length'; |
| 35 | return result; |
| 36 | } |
| 37 | |
| 38 | if (checkLength < 1) { |
| 39 | result.errorMessages = 'interanl error: csvHeaderNames, jsonKeyNames, and mandatory arrays have no entries'; |
| 40 | return result; |
| 41 | } |
| 42 | |
| 43 | // Make a nice string to print in the error case to tell user what is the |
| 44 | // required heaer row format |
| 45 | |
| 46 | for (let i = 0; i < csvHeaderNames.length; ++i) { |
| 47 | if (i === 0) { |
| 48 | printDictKeys = csvHeaderNames[i]; |
| 49 | } else { |
| 50 | printDictKeys += ',' + csvHeaderNames[i]; |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | let dictElems = rawCsvData.split('\n'); |
| 55 | let numColumns = 0; |
| 56 | let filteredDictElems = []; |
| 57 | |
| 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. |
| 62 | |
| 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. |
| 73 | |
| 74 | for (let i = 0; i < dictElems.length; i++) { |
| 75 | |
| 76 | let oneRow = dictElems[i].trim(); |
| 77 | let j = 0; |
| 78 | let inQuote = false |
| 79 | let nextChar = undefined; |
| 80 | let prevChar = null; |
| 81 | |
| 82 | |
| 83 | if (oneRow === '') { |
| 84 | continue; // Skip blank rows |
| 85 | } else if (oneRow.indexOf(internalDelimiter) !== -1) { |
| 86 | result.errorMessages += '\nRow #' + i + ' contains illegal sequence of characters (' + internalDelimiter + ')'; |
| 87 | break; |
| 88 | } else { |
| 89 | nextChar = oneRow[1]; |
| 90 | } |
| 91 | |
| 92 | let newStr = ''; |
| 93 | numColumns = 1; |
| 94 | |
| 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. |
| 101 | |
| 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; |
| 107 | break; |
| 108 | } else { |
| 109 | inQuote = true; |
| 110 | } |
| 111 | } else { |
| 112 | if (nextChar === '"') { |
| 113 | newStr += '"'; |
| 114 | ++j; |
| 115 | } else if ((nextChar !== delimiter) && (nextChar !== undefined)) { |
| 116 | result.errorMessages += '\nRow #' + (i + 1) + ' is badly formatted at column #' + numColumns + '. Perhaps an unescaped double quote.'; |
| 117 | break; |
| 118 | } else if (nextChar === delimiter) { |
| 119 | ++numColumns; |
| 120 | inQuote = false; |
| 121 | newStr += internalDelimiter; |
| 122 | prevChar = delimiter; |
| 123 | j += 2; |
| 124 | nextChar = oneRow[j + 1]; |
| 125 | continue; |
| 126 | } else { |
| 127 | ++numColumns; |
| 128 | inQuote = false; |
| 129 | break; |
| 130 | } |
| 131 | } |
| 132 | } else { |
| 133 | if (oneRow[j] === delimiter && inQuote === false) { |
| 134 | newStr += internalDelimiter; |
| 135 | ++numColumns; |
| 136 | } else { |
| 137 | newStr += oneRow[j]; |
| 138 | } |
| 139 | } |
| 140 | prevChar = oneRow[j]; |
| 141 | ++j; |
| 142 | nextChar = oneRow[j + 1]; // can result in undefined at the end |
| 143 | } |
| 144 | |
| 145 | if (result.errorMessages === '' && inQuote !== false) { |
| 146 | result.errorMessages += '\nMismatched double quotes at row #' + (i + 1); |
| 147 | break; |
| 148 | } else if (result.errorMessages === '' && numColumns < jsonKeyNames.length) { |
| 149 | result.errorMessages += '\nNot enough columns (' + jsonKeyNames.length + ') at row #' + (i + 1); |
| 150 | break; |
| 151 | } |
| 152 | |
| 153 | filteredDictElems.push(newStr); |
| 154 | } |
| 155 | |
| 156 | if (result.errorMessages !== '') { |
| 157 | return result; |
| 158 | } |
| 159 | |
| 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'; |
| 163 | return result; |
| 164 | } |
| 165 | |
| 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 |
| 168 | // checks pass. |
| 169 | |
| 170 | let headers = filteredDictElems[0].split(internalDelimiter); |
| 171 | |
| 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; |
| 176 | return result; |
| 177 | } |
| 178 | } |
| 179 | |
| 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. |
| 182 | |
| 183 | for (let i = 1; i < filteredDictElems.length; i++) { |
| 184 | let data = filteredDictElems[i].split(internalDelimiter); |
| 185 | let obj = {}; |
| 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.'; |
| 190 | } |
| 191 | obj[jsonKeyNames[j]] = value; |
| 192 | } |
| 193 | result.jsonObjArray.push(obj); |
| 194 | } |
| 195 | |
| 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 = []; |
| 201 | } |
| 202 | |
| 203 | return result; |
| 204 | } |