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 if (csvType == CsvType.SEMICOLON) {
227            format = CSVFormat.Builder.create(CSVFormat.RFC4180).setDelimiter(';').build();
228        } else {
229            buffered.close();
230            throw new IOException("Unrecognized CSV Format");
231        }
232        returnList = parseCSV(buffered, format);
233        rdr.close();
234        return returnList;
235    }
236
237    /**
238     * {@inheritDoc}
239     */
240    @Override
241    public void storeTableAsCSV(@Nonnull File file) throws FileNotFoundException {
242        _internalTable.storeTableAsCSV(file, getSystemName(), getUserName());
243    }
244
245    /**
246     * {@inheritDoc}
247     */
248    @Override
249    public void storeTableAsCSV(@Nonnull File file,
250                                @CheckForNull String systemName,
251                                @CheckForNull String userName)
252            throws FileNotFoundException {
253
254        _internalTable.storeTableAsCSV(file, systemName, userName);
255    }
256
257    @Override
258    public void setState(int s) throws JmriException {
259        _state = s;
260    }
261
262    @Override
263    public int getState() {
264        return _state;
265    }
266
267    @Override
268    public String getBeanType() {
269        return Bundle.getMessage("BeanNameTable");
270        //        return Manager.LOGIXNGS;
271        //        return NamedTable.class;
272    }
273
274    /**
275     * {@inheritDoc}
276     */
277    @Override
278    public Object getCell(int row, int column) {
279        return _internalTable.getCell(row, column);
280    }
281
282    /**
283     * {@inheritDoc}
284     */
285    @Override
286    public void setCell(Object value, int row, int column) {
287        _internalTable.setCell(value, row, column);
288    }
289
290    /**
291     * {@inheritDoc}
292     */
293    @Override
294    public int numRows() {
295        return _internalTable.numRows();
296    }
297
298    /**
299     * {@inheritDoc}
300     */
301    @Override
302    public int numColumns() {
303        return _internalTable.numColumns();
304    }
305
306    /**
307     * {@inheritDoc}
308     */
309    @Override
310    public int getRowNumber(String rowName) {
311        return _internalTable.getRowNumber(rowName);
312    }
313
314    /**
315     * {@inheritDoc}
316     */
317    @Override
318    public int getColumnNumber(String columnName) {
319        return _internalTable.getColumnNumber(columnName);
320    }
321
322    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractNamedTable.class);
323
324/*
325    protected void insertColumn(int col) {
326        _internalTable.insertColumn(col);
327    }
328
329    protected void deleteColumn(int col) {
330        _internalTable.deleteColumn(col);
331    }
332
333    protected void insertRow(int row) {
334        _internalTable.insertRow(row);
335    }
336
337    protected void deleteRow(int row) {
338        _internalTable.deleteRow(row);
339    }
340*/
341}