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