001package jmri.jmrit.operations.trains;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.io.*;
006import java.nio.charset.StandardCharsets;
007import java.text.ParseException;
008import java.text.SimpleDateFormat;
009import java.util.*;
010
011import org.apache.commons.csv.CSVFormat;
012import org.apache.commons.csv.CSVPrinter;
013import org.slf4j.Logger;
014import org.slf4j.LoggerFactory;
015
016import jmri.InstanceManager;
017import jmri.InstanceManagerAutoDefault;
018import jmri.jmrit.XmlFile;
019import jmri.jmrit.operations.setup.*;
020import jmri.jmrit.operations.setup.backup.AutoBackup;
021import jmri.jmrit.operations.setup.backup.DefaultBackup;
022
023/**
024 * Logs train movements and status to a file.
025 *
026 * @author Daniel Boudreau Copyright (C) 2010, 2013, 2024
027 */
028public class TrainLogger extends XmlFile implements InstanceManagerAutoDefault, PropertyChangeListener {
029
030    File _fileLogger;
031    private boolean _trainLog = false; // when true logging train movements
032
033    public TrainLogger() {
034    }
035
036    public void enableTrainLogging(boolean enable) {
037        if (enable) {
038            addTrainListeners();
039        } else {
040            removeTrainListeners();
041        }
042    }
043
044    private void createFile() {
045        if (!Setup.isTrainLoggerEnabled()) {
046            return;
047        }
048        if (_fileLogger != null) {
049            return; // log file has already been created
050        } // create the logging file for this session
051        try {
052            if (!checkFile(getFullLoggerFileName())) {
053                // The file/directory does not exist, create it before writing
054                _fileLogger = new java.io.File(getFullLoggerFileName());
055                File parentDir = _fileLogger.getParentFile();
056                if (!parentDir.exists()) {
057                    if (!parentDir.mkdirs()) {
058                        log.error("logger directory not created");
059                    }
060                }
061                if (_fileLogger.createNewFile()) {
062                    log.debug("new file created");
063                    // add header
064                    fileOut(getHeader());
065                }
066            } else {
067                _fileLogger = new java.io.File(getFullLoggerFileName());
068            }
069        } catch (Exception e) {
070            log.error("Exception while making logging directory", e);
071        }
072
073    }
074
075    private void store(Train train) {
076        // create train file if needed
077        createFile();
078        // Note that train status can contain a comma
079        List<Object> line = Arrays.asList(new Object[]{train.getName(),
080                train.getDescription(),
081                train.getCurrentLocationName(),
082                train.getNextLocationName(),
083                train.getNumberCarsInTrain(),
084                train.getNumberCarsPickedUp(),
085                train.getNumberCarsSetout(),
086                train.getTrainLength(),
087                train.getTrainWeight(),
088                train.getStatus(),
089                train.getBuildFailedMessage(),
090                getDateAndTime()});
091        fileOut(line);
092    }
093
094    ResourceBundle rb = ResourceBundle
095            .getBundle("jmri.jmrit.operations.setup.JmritOperationsSetupBundle");
096
097    /*
098     * Adds a status line to the log file whenever the trains file is saved.
099     */
100    private void storeFileSaved() {
101        List<Object> line = Arrays.asList(new Object[]{
102                Bundle.getMessage("TrainLogger"), // train name
103                "", // train description
104                "", // current location
105                "", // next location name
106                "", // cars
107                "", // pulls
108                "", // drops
109                "", // length
110                "", // weight
111                Setup.isAutoSaveEnabled() ? rb.getString("AutoSave") : Bundle.getMessage("Manual"), // status
112                Bundle.getMessage("TrainsSaved"), // build messages
113                getDateAndTime()});
114        fileOut(line);
115    }
116
117    private void storeBackupChanged(PropertyChangeEvent e) {
118        // create train file if needed
119        createFile();
120        List<Object> line = Arrays.asList(new Object[]{
121                Bundle.getMessage("TrainLogger"), // train name
122                "", // train description
123                "", // current location
124                "", // next location name
125                "", // cars
126                "", // pulls
127                "", // drops
128                "", // length
129                e.getPropertyName(), // weight
130                e.getOldValue(), // status
131                e.getNewValue(), // build messages
132                getDateAndTime()});
133        fileOut(line);
134    }
135
136    private List<Object> getHeader() {
137        return Arrays.asList(new Object[]{Bundle.getMessage("Name"),
138                Bundle.getMessage("Description"),
139                Bundle.getMessage("Current"),
140                Bundle.getMessage("NextLocation"),
141                Bundle.getMessage("Cars"),
142                Bundle.getMessage("Pulls"),
143                Bundle.getMessage("Drops"),
144                Bundle.getMessage("Length"),
145                Bundle.getMessage("Weight"),
146                Bundle.getMessage("Status"),
147                Bundle.getMessage("BuildMessages"),
148                Bundle.getMessage("DateAndTime")});
149    }
150
151    /*
152     * Appends one line to file.
153     */
154    private void fileOut(List<Object> line) {
155        if (_fileLogger == null) {
156            log.error("Log file doesn't exist");
157            return;
158        }
159
160        // FileOutputStream is set to append
161        try (CSVPrinter fileOut = new CSVPrinter(new BufferedWriter(new OutputStreamWriter(
162                new FileOutputStream(_fileLogger, true), StandardCharsets.UTF_8)), CSVFormat.DEFAULT)) {
163            log.debug("Log: {}", line);
164            fileOut.printRecord(line);
165            fileOut.flush();
166            fileOut.close();
167        } catch (IOException e) {
168            log.error("Exception while opening log file: {}", e.getLocalizedMessage());
169        }
170    }
171
172    private void addTrainListeners() {
173        if (Setup.isTrainLoggerEnabled() && !_trainLog) {
174            log.debug("Train Logger adding train listerners");
175            _trainLog = true;
176            List<Train> trains = InstanceManager.getDefault(TrainManager.class).getTrainsByIdList();
177            trains.forEach(train -> train.addPropertyChangeListener(this));
178            // listen for new trains being added
179            InstanceManager.getDefault(TrainManager.class).addPropertyChangeListener(this);
180            // listen for backup file changes
181            InstanceManager.getDefault(DefaultBackup.class).addPropertyChangeListener(this);
182            InstanceManager.getDefault(AutoBackup.class).addPropertyChangeListener(this);
183        }
184    }
185
186    private void removeTrainListeners() {
187        log.debug("Train Logger removing train listerners");
188        if (_trainLog) {
189            List<Train> trains = InstanceManager.getDefault(TrainManager.class).getTrainsByIdList();
190            trains.forEach(train -> train.removePropertyChangeListener(this));
191            InstanceManager.getDefault(TrainManager.class).removePropertyChangeListener(this);
192            InstanceManager.getDefault(DefaultBackup.class).removePropertyChangeListener(this);
193            InstanceManager.getDefault(AutoBackup.class).removePropertyChangeListener(this);
194        }
195        _trainLog = false;
196    }
197
198    public void dispose() {
199        removeTrainListeners();
200    }
201
202    @Override
203    public void propertyChange(PropertyChangeEvent e) {
204        if (e.getPropertyName().equals(Train.TRAIN_CURRENT_CHANGED_PROPERTY) && e.getNewValue() != null ||
205                e.getPropertyName().equals(Train.STATUS_CHANGED_PROPERTY) &&
206                        (e.getNewValue().equals(Train.TRAIN_RESET) ||
207                                e.getNewValue().equals(Train.BUILDING) ||
208                                e.getNewValue().equals(Train.BUILD_FAILED) ||
209                                e.getNewValue().toString().startsWith(Train.TERMINATED))) {
210            if (Control.SHOW_PROPERTY) {
211                log.debug("Train logger sees property change for train ({}), property name: {}", e.getSource(),
212                        e.getPropertyName());
213            }
214            store((Train) e.getSource());
215        }
216        if (e.getPropertyName().equals(TrainManager.LISTLENGTH_CHANGED_PROPERTY)) {
217            if ((Integer) e.getNewValue() > (Integer) e.getOldValue()) {
218                // a car or engine has been added
219                removeTrainListeners();
220                addTrainListeners();
221            }
222        }
223        if (e.getPropertyName().equals(TrainManager.TRAINS_SAVED_PROPERTY)) {
224            storeFileSaved();
225        }
226        if (e.getPropertyName().equals(DefaultBackup.COPY_FILES_CHANGED_PROPERTY)) {
227            storeBackupChanged(e);
228        }
229    }
230
231    public String getFullLoggerFileName() {
232        return loggingDirectory + File.separator + getFileName();
233    }
234
235    private String operationsDirectory =
236            OperationsSetupXml.getFileLocation() + OperationsSetupXml.getOperationsDirectoryName();
237    private String loggingDirectory = operationsDirectory + File.separator + "logger" + File.separator + "trains"; // NOI18N
238
239    public String getDirectoryName() {
240        return loggingDirectory;
241    }
242
243    public void setDirectoryName(String name) {
244        loggingDirectory = name;
245    }
246
247    private String fileName;
248
249    public String getFileName() {
250        if (fileName == null) {
251            fileName = Bundle.getMessage("Trains") + "_" + getDate() + ".csv"; // NOI18N
252        }
253        return fileName;
254    }
255
256    private String getDate() {
257        Date date = Calendar.getInstance().getTime();
258        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy_MM_dd"); // NOI18N
259        return simpleDateFormat.format(date);
260    }
261
262    /**
263     * Return the date and time in an MS Excel friendly format yyyy/MM/dd
264     * HH:mm:ss
265     */
266    private String getDateAndTime() {
267        String time = Calendar.getInstance().getTime().toString();
268        SimpleDateFormat dt = new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy"); // NOI18N
269        SimpleDateFormat dtout = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); // NOI18N
270        try {
271            return dtout.format(dt.parse(time));
272        } catch (ParseException e) {
273            return time; // there was an issue, use the old format
274        }
275    }
276
277    private final static Logger log = LoggerFactory.getLogger(TrainLogger.class);
278}