001package jmri.jmrit.logixng.implementation;
002
003import java.io.*;
004import java.util.ArrayList;
005import java.util.Iterator;
006import java.util.List;
007
008import javax.annotation.CheckForNull;
009import javax.annotation.Nonnull;
010
011import jmri.InstanceManager;
012import jmri.JmriException;
013import jmri.Manager;
014import jmri.NamedBean;
015import jmri.implementation.AbstractNamedBean;
016import jmri.jmrit.logixng.AnonymousTable;
017import jmri.jmrit.logixng.NamedTable;
018import jmri.jmrit.logixng.NamedTableManager;
019import jmri.util.FileUtil;
020import org.apache.commons.csv.CSVRecord;
021import org.apache.commons.io.FileUtils;
022import org.apache.commons.io.input.BOMInputStream;
023import org.apache.commons.csv.CSVFormat;
024import org.apache.commons.io.input.CharSequenceReader;
025
026/**
027 * The default implementation of a NamedTable
028 *
029 * @author Daniel Bergqvist 2018
030 * @author J. Scott Walton (c) 2022 (Csv Types)
031 */
032public abstract class AbstractNamedTable extends AbstractNamedBean implements NamedTable {
033
034    private int _state = NamedBean.UNKNOWN;
035    protected final AnonymousTable _internalTable;
036
037    /**
038     * Create a new named table.
039     *
040     * @param sys        the system name
041     * @param user       the user name or null if no user name
042     * @param numRows    the number or rows in the table
043     * @param numColumns the number of columns in the table
044     */
045    public AbstractNamedTable(@Nonnull String sys,
046                              @CheckForNull String user,
047                              int numRows,
048                              int numColumns)
049            throws BadUserNameException, BadSystemNameException {
050        super(sys, user);
051        _internalTable = new DefaultAnonymousTable(numRows, numColumns);
052    }
053
054    /**
055     * Create a new named table with an existing array of cells.
056     * Row 0 has the column names and column 0 has the row names.
057     *
058     * @param systemName the system name
059     * @param userName   the user name
060     * @param data       the data in the table. Note that this data is not
061     *                   copied to a new array but used by the table as is.
062     */
063    public AbstractNamedTable(@Nonnull String systemName,
064                              @CheckForNull String userName,
065                              @Nonnull Object[][] data)
066            throws BadUserNameException, BadSystemNameException {
067        super(systemName, userName);
068
069        // Do this test here to ensure all the tests are using correct system names
070        Manager.NameValidity isNameValid = InstanceManager.getDefault(NamedTableManager.class).validSystemNameFormat(mSystemName);
071        if (isNameValid != Manager.NameValidity.VALID) {
072            throw new IllegalArgumentException("system name is not valid");
073        }
074        _internalTable = new DefaultAnonymousTable(data);
075    }
076
077    /**
078     * Create a new named table with an existing array of cells.
079     * Row 0 has the column names and column 0 has the row names.
080     *
081     * @param systemName the system name
082     * @param userName   the user name
083     * @param fileName   the file name of the CSV table
084     * @param data       the data in the table. Note that this data is not
085     *                   copied to a new array but used by the table as is.
086     */
087    public AbstractNamedTable(@Nonnull String systemName,
088                              @CheckForNull String userName,
089                              @Nonnull String fileName,
090                              @Nonnull Object[][] data)
091            throws BadUserNameException, BadSystemNameException {
092        super(systemName, userName);
093
094        // Do this test here to ensure all the tests are using correct system names
095        Manager.NameValidity isNameValid = InstanceManager.getDefault(NamedTableManager.class).validSystemNameFormat(mSystemName);
096        if (isNameValid != Manager.NameValidity.VALID) {
097            throw new IllegalArgumentException("system name is not valid");
098        }
099        _internalTable = new DefaultAnonymousTable(data);
100    }
101
102    @Nonnull
103    private static NamedTable loadFromCSV(@Nonnull String systemName,
104                                          @CheckForNull String userName,
105                                          @CheckForNull String fileName,
106                                          @Nonnull List<List<String>> lines,
107                                          boolean registerInManager,
108                                          CsvType csvType)
109            throws NamedBean.BadUserNameException, NamedBean.BadSystemNameException {
110
111        NamedTableManager manager = InstanceManager.getDefault(NamedTableManager.class);
112
113        if (userName != null && userName.isEmpty()) {
114            userName = null;
115        }
116
117        // First row is column names.
118        int numRows = lines.size() - 1;
119
120        // If the last row is empty string, ignore it.
121        if (lines.get(lines.size() - 1).isEmpty()) {
122            numRows--;
123        }
124
125        int numColumns = 0;
126
127        String[][] csvCells = new String[numRows + 1][];
128        for (int rowCount = 0; rowCount < numRows + 1; rowCount++) {
129            csvCells[rowCount] = lines.get(rowCount).toArray(new String[0]);
130            numColumns = Math.max(numColumns, csvCells[rowCount].length);
131        }
132
133        // Ensure all rows have same number of columns
134        log.debug("about to verify csvCells -- size is {}", numRows);
135        for (int rowCount = 0; rowCount < numRows + 1; rowCount++) {
136            Object[] cells = csvCells[rowCount];
137            if (cells.length < numColumns) {
138                String[] newCells = new String[numColumns];
139                System.arraycopy(cells, 0, newCells, 0, cells.length);
140                csvCells[rowCount] = newCells;
141                for (int i = cells.length; i < numColumns; i++)
142                {
143                    newCells[i] = "";
144                }
145                csvCells[rowCount] = newCells;
146            }
147        }
148
149        NamedTable table = new DefaultCsvNamedTable(systemName, userName, fileName, csvCells, csvType);
150
151        if (registerInManager) {
152            manager.register(table);
153        }
154        return table;
155    }
156
157    private static List<List<String>> parseCSV(Reader rdr, CSVFormat format) throws IOException {
158        List<List<String>> returnList = new ArrayList<>();
159        Iterable<CSVRecord> records = format.parse(rdr);
160        records.forEach(record -> {
161            ArrayList<String> currentList = new ArrayList<>();
162            Iterator<String> itemList = record.iterator();
163            itemList.forEachRemaining(item -> {
164                currentList.add(item);
165            });
166            returnList.add(currentList);
167        });
168        return returnList;
169    }
170
171    @Nonnull
172    public static NamedTable loadTableFromCSV_Text(@Nonnull String systemName,
173                                                   @CheckForNull String userName,
174                                                   @Nonnull String text,
175                                                   boolean registerInManager,
176                                                   CsvType csvType)
177            throws BadUserNameException, BadSystemNameException, IOException{
178
179        //List<String> lines = Arrays.asList(text.split("\\r?\\n", -1));
180        Reader rdr = new CharSequenceReader(text);
181        List<List<String>> lines = parseCSV(rdr, CSVFormat.TDF);
182        return loadFromCSV(systemName, userName, null, lines, registerInManager, csvType);
183    }
184
185    @Nonnull
186    public static NamedTable loadTableFromCSV_File(@Nonnull String systemName,
187                                                   @CheckForNull String userName,
188                                                   @Nonnull String fileName,
189                                                   boolean registerInManager,
190                                                   CsvType csvType)
191            throws NamedBean.BadUserNameException, NamedBean.BadSystemNameException, IOException {
192
193        //List<String> lines = Files.readAllLines(FileUtil.getFile(fileName).toPath(), StandardCharsets.UTF_8);
194        List<List<String>> lines = readIt(FileUtil.getFile(fileName),  csvType);
195        return loadFromCSV(systemName, userName, fileName, lines, registerInManager, csvType);
196    }
197
198    @Nonnull
199    public static NamedTable loadTableFromCSV_File(@Nonnull String systemName,
200                                                   @CheckForNull String userName,
201                                                   @Nonnull File file,
202                                                   boolean registerInManager,
203                                                   CsvType csvType)
204            throws NamedBean.BadUserNameException, NamedBean.BadSystemNameException, IOException {
205
206        //List<String> lines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
207        List<List<String>> lines = readIt(file, csvType);
208        return loadFromCSV(systemName, userName, file.getPath(), lines, registerInManager, csvType);
209    }
210
211    private static List<List<String>> readIt(File infile, CsvType csvType) throws IOException {
212        List<List<String>> returnList = null;
213        InputStream in = null;
214        in = FileUtils.openInputStream(infile);
215        BOMInputStream bomInputStream = new BOMInputStream(in);
216        if (bomInputStream.hasBOM()) {
217            log.debug("Input file has a Byte Order Marker attached");
218        }
219        InputStreamReader rdr = new InputStreamReader(bomInputStream);
220        BufferedReader buffered = new BufferedReader(rdr);
221        CSVFormat format = null;
222        if (csvType == CsvType.TABBED) {
223            format = CSVFormat.TDF;
224        } else if (csvType == CsvType.COMMA) {
225            format = CSVFormat.RFC4180;
226        } else {
227            buffered.close();
228            throw new IOException("Unrecognized CSV Format");
229        }
230        returnList = parseCSV(buffered, format);
231        rdr.close();
232        return returnList;
233    }
234
235
236    /**
237     * {@inheritDoc}
238     */
239    @Override
240    public void storeTableAsCSV(@Nonnull File file) throws FileNotFoundException {
241        _internalTable.storeTableAsCSV(file, getSystemName(), getUserName());
242    }
243
244    /**
245     * {@inheritDoc}
246     */
247    @Override
248    public void storeTableAsCSV(@Nonnull File file,
249                                @CheckForNull String systemName,
250                                @CheckForNull String userName)
251            throws FileNotFoundException {
252
253        _internalTable.storeTableAsCSV(file, systemName, userName);
254    }
255
256    @Override
257    public void setState(int s) throws JmriException {
258        _state = s;
259    }
260
261    @Override
262    public int getState() {
263        return _state;
264    }
265
266    @Override
267    public String getBeanType() {
268        return Bundle.getMessage("BeanNameTable");
269        //        return Manager.LOGIXNGS;
270        //        return NamedTable.class;
271    }
272
273    /**
274     * {@inheritDoc}
275     */
276    @Override
277    public Object getCell(int row, int column) {
278        return _internalTable.getCell(row, column);
279    }
280
281    /**
282     * {@inheritDoc}
283     */
284    @Override
285    public void setCell(Object value, int row, int column) {
286        _internalTable.setCell(value, row, column);
287    }
288
289    /**
290     * {@inheritDoc}
291     */
292    @Override
293    public int numRows() {
294        return _internalTable.numRows();
295    }
296
297    /**
298     * {@inheritDoc}
299     */
300    @Override
301    public int numColumns() {
302        return _internalTable.numColumns();
303    }
304
305    /**
306     * {@inheritDoc}
307     */
308    @Override
309    public int getRowNumber(String rowName) {
310        return _internalTable.getRowNumber(rowName);
311    }
312
313    /**
314     * {@inheritDoc}
315     */
316    @Override
317    public int getColumnNumber(String columnName) {
318        return _internalTable.getColumnNumber(columnName);
319    }
320
321    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractNamedTable.class);
322
323/*    
324    protected void insertColumn(int col) {
325        _internalTable.insertColumn(col);
326    }
327    
328    protected void deleteColumn(int col) {
329        _internalTable.deleteColumn(col);
330    }
331    
332    protected void insertRow(int row) {
333        _internalTable.insertRow(row);
334    }
335    
336    protected void deleteRow(int row) {
337        _internalTable.deleteRow(row);
338    }
339*/
340}