Address Manage Dictionary issues
[clamp.git] / ui-react / src / utils / CsvToJson.js
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 }