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