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.getStatus(),
082                train.getBuildFailedMessage(),
083                getTime()});
084        fileOut(line);
085    }
086
087    ResourceBundle rb = ResourceBundle
088            .getBundle("jmri.jmrit.operations.setup.JmritOperationsSetupBundle");
089
090    /*
091     * Adds a status line to the log file whenever the trains file is saved.
092     */
093    private void storeFileSaved() {
094        if (_fileLogger == null) {
095            return;
096        }
097        List<Object> line = Arrays.asList(new Object[]{
098                Bundle.getMessage("TrainLogger"), // train name
099                "", // train description
100                "", // current location
101                "", // next location name
102                Setup.isAutoSaveEnabled() ? rb.getString("AutoSave") : Bundle.getMessage("Manual"), // status
103                Bundle.getMessage("TrainsSaved"), // build messages
104                getTime()});
105        fileOut(line);
106    }
107
108    private List<Object> getHeader() {
109        return Arrays.asList(new Object[]{Bundle.getMessage("Name"),
110                Bundle.getMessage("Description"),
111                Bundle.getMessage("Current"),
112                Bundle.getMessage("NextLocation"),
113                Bundle.getMessage("Status"),
114                Bundle.getMessage("BuildMessages"),
115                Bundle.getMessage("DateAndTime")});
116    }
117
118    /*
119     * Appends one line to file.
120     */
121    private void fileOut(List<Object> line) {
122        if (_fileLogger == null) {
123            log.error("Log file doesn't exist");
124            return;
125        }
126
127        // FileOutputStream is set to append
128        try (CSVPrinter fileOut = new CSVPrinter(new BufferedWriter(new OutputStreamWriter(
129                new FileOutputStream(_fileLogger, true), StandardCharsets.UTF_8)), CSVFormat.DEFAULT)) {
130            log.debug("Log: {}", line);
131            fileOut.printRecord(line);
132            fileOut.flush();
133            fileOut.close();
134        } catch (IOException e) {
135            log.error("Exception while opening log file: {}", e.getLocalizedMessage());
136        }
137    }
138
139    private void addTrainListeners() {
140        if (Setup.isTrainLoggerEnabled() && !_trainLog) {
141            log.debug("Train Logger adding train listerners");
142            _trainLog = true;
143            List<Train> trains = InstanceManager.getDefault(TrainManager.class).getTrainsByIdList();
144            trains.forEach(train -> train.addPropertyChangeListener(this));
145            // listen for new trains being added
146            InstanceManager.getDefault(TrainManager.class).addPropertyChangeListener(this);
147        }
148    }
149
150    private void removeTrainListeners() {
151        log.debug("Train Logger removing train listerners");
152        if (_trainLog) {
153            List<Train> trains = InstanceManager.getDefault(TrainManager.class).getTrainsByIdList();
154            trains.forEach(train -> train.removePropertyChangeListener(this));
155            InstanceManager.getDefault(TrainManager.class).removePropertyChangeListener(this);
156        }
157        _trainLog = false;
158    }
159
160    public void dispose() {
161        removeTrainListeners();
162    }
163
164    @Override
165    public void propertyChange(PropertyChangeEvent e) {
166        if (e.getPropertyName().equals(Train.STATUS_CHANGED_PROPERTY) ||
167                e.getPropertyName().equals(Train.TRAIN_LOCATION_CHANGED_PROPERTY)) {
168            if (Control.SHOW_PROPERTY) {
169                log.debug("Train logger sees property change for train {}", e.getSource());
170            }
171            store((Train) e.getSource());
172        }
173        if (e.getPropertyName().equals(TrainManager.LISTLENGTH_CHANGED_PROPERTY)) {
174            if ((Integer) e.getNewValue() > (Integer) e.getOldValue()) {
175                // a car or engine has been added
176                removeTrainListeners();
177                addTrainListeners();
178            }
179        }
180        if (e.getPropertyName().equals(TrainManager.TRAINS_SAVED_PROPERTY)) {
181            storeFileSaved();
182        }
183    }
184
185    public String getFullLoggerFileName() {
186        return loggingDirectory + File.separator + getFileName();
187    }
188
189    private String operationsDirectory =
190            OperationsSetupXml.getFileLocation() + OperationsSetupXml.getOperationsDirectoryName();
191    private String loggingDirectory = operationsDirectory + File.separator + "logger" + File.separator + "trains"; // NOI18N
192
193    public String getDirectoryName() {
194        return loggingDirectory;
195    }
196
197    public void setDirectoryName(String name) {
198        loggingDirectory = name;
199    }
200
201    private String fileName;
202
203    public String getFileName() {
204        if (fileName == null) {
205            fileName = Bundle.getMessage("Trains") + "_" + getDate() + ".csv"; // NOI18N
206        }
207        return fileName;
208    }
209
210    private String getDate() {
211        Date date = Calendar.getInstance().getTime();
212        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy_MM_dd"); // NOI18N
213        return simpleDateFormat.format(date);
214    }
215
216    /**
217     * Return the date and time in an MS Excel friendly format yyyy/MM/dd
218     * HH:mm:ss
219     */
220    private String getTime() {
221        String time = Calendar.getInstance().getTime().toString();
222        SimpleDateFormat dt = new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy"); // NOI18N
223        SimpleDateFormat dtout = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); // NOI18N
224        try {
225            return dtout.format(dt.parse(time));
226        } catch (ParseException e) {
227            return time; // there was an issue, use the old format
228        }
229    }
230
231    private final static Logger log = LoggerFactory.getLogger(TrainLogger.class);
232}