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