001package jmri.jmrit.operations.rollingstock;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.io.*;
006import java.nio.charset.StandardCharsets;
007import java.time.LocalDateTime;
008import java.time.format.DateTimeFormatter;
009import java.util.Arrays;
010import java.util.List;
011
012import jmri.InstanceManager;
013import jmri.InstanceManagerAutoDefault;
014import jmri.jmrit.XmlFile;
015import jmri.jmrit.operations.OperationsXml;
016import jmri.jmrit.operations.rollingstock.cars.Car;
017import jmri.jmrit.operations.rollingstock.cars.CarManager;
018import jmri.jmrit.operations.rollingstock.engines.Engine;
019import jmri.jmrit.operations.rollingstock.engines.EngineManager;
020import jmri.jmrit.operations.setup.*;
021
022import org.apache.commons.csv.CSVFormat;
023import org.apache.commons.csv.CSVPrinter;
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027/**
028 * Logs rolling stock movements by writing their locations to a file.
029 *
030 * @author Daniel Boudreau Copyright (C) 2010, 2016
031 */
032public class RollingStockLogger extends XmlFile implements InstanceManagerAutoDefault, PropertyChangeListener {
033
034    private boolean engLog = false; // when true logging engine movements
035    private boolean carLog = false; // when true logging car movements
036
037    public RollingStockLogger() {
038        // nothing to do
039    }
040
041    public void enableCarLogging(boolean enable) {
042        if (enable) {
043            addCarListeners();
044        } else {
045            removeCarListeners();
046        }
047    }
048
049    public void enableEngineLogging(boolean enable) {
050        if (enable) {
051            addEngineListeners();
052        } else {
053            removeEngineListeners();
054        }
055    }
056
057    private boolean mustHaveTrack = true; // when true only updates that have a track are saved
058
059    private void store(RollingStock rs) {
060
061        if (rs.getTrack() == null && mustHaveTrack) {
062            return;
063        }
064
065        String carLoad = "";
066        String carFinalDest = "";
067        String carFinalDestTrack = "";
068        if (Car.class.isInstance(rs)) {
069            Car car = (Car) rs;
070            carLoad = car.getLoadName();
071            carFinalDest = car.getFinalDestinationName();
072            carFinalDestTrack = car.getFinalDestinationTrackName();
073        }
074
075        List<Object> line = Arrays.asList(new Object[]{rs.getNumber(),
076            rs.getRoadName(),
077            rs.getTypeName(),
078            carLoad,
079            rs.getLocationName(),
080            rs.getTrackName(),
081            carFinalDest,
082            carFinalDestTrack,
083            rs.getTrainName(),
084            rs.getMoves(),
085            getTime()});
086
087        fileOut(line); // append line to common file
088        fileOut(line, rs); // append line to individual file
089    }
090
091    /*
092     * Appends one line to common log file.
093     */
094    private void fileOut(List<Object> line) {
095        fileOut(line, getFile());
096    }
097
098    /*
099     * Appends one line to the rolling stock's individual file.
100     */
101    private void fileOut(List<Object> line, RollingStock rs) {
102        fileOut(line, getFile(rs));
103    }
104
105    private void fileOut(List<Object> line, File file) {
106        // FileOutputStream is set to append
107        try (CSVPrinter fileOut = new CSVPrinter(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, true),
108                    StandardCharsets.UTF_8)), CSVFormat.DEFAULT)) {
109            log.debug("Log: {}", line);
110            fileOut.printRecord(line);
111        } catch (IOException e) {
112            log.error("Exception while opening log file: {}", e.getLocalizedMessage());
113        }
114    }
115
116    /*
117     * Returns the common log file for all rolling stock
118     */
119    private File getFile() {
120        File fileLogger = null;
121        if (Setup.isEngineLoggerEnabled() || Setup.isCarLoggerEnabled()) {
122            try {
123                if (!checkFile(getFullLoggerFileName())) {
124                    // The file/directory does not exist, create it before writing
125                    fileLogger = new java.io.File(getFullLoggerFileName());
126                    File parentDir = fileLogger.getParentFile();
127                    if (!parentDir.exists()) {
128                        if (!parentDir.mkdirs()) {
129                            log.error("logger directory not created");
130                        }
131                    }
132                    if (fileLogger.createNewFile()) {
133                        log.debug("new file created");
134                        // add header
135                        fileOut(getHeader());
136                    }
137                } else {
138                    fileLogger = new java.io.File(getFullLoggerFileName());
139                }
140            } catch (IOException e) {
141                log.error("Exception while making logging directory: {}", e.getLocalizedMessage());
142            }
143        }
144        return fileLogger;
145    }
146
147    private List<Object> getHeader() {
148        return Arrays.asList(new Object[]{Bundle.getMessage("Number"),
149            Bundle.getMessage("Road"),
150            Bundle.getMessage("Type"),
151            Bundle.getMessage("Load"),
152            Bundle.getMessage("Location"),
153            Bundle.getMessage("Track"),
154            Bundle.getMessage("FinalDestination"),
155            Bundle.getMessage("Track"),
156            Bundle.getMessage("Train"),
157            Bundle.getMessage("Moves"),
158            Bundle.getMessage("DateAndTime")});
159    }
160
161    /*
162     * Gets the individual log file for a specific car or loco.
163     */
164    private File getFile(RollingStock rs) {
165        File file = null;
166        if (Setup.isEngineLoggerEnabled() || Setup.isCarLoggerEnabled()) {
167            // create the logging file for this rolling stock
168            try {
169                if (!checkFile(getFullLoggerFileName(rs))) {
170                    // The file/directory does not exist, create it before writing
171                    file = new java.io.File(getFullLoggerFileName(rs));
172                    File parentDir = file.getParentFile();
173                    if (!parentDir.exists()) {
174                        if (!parentDir.mkdirs()) {
175                            log.error("logger directory not created");
176                        }
177                    }
178                    if (file.createNewFile()) {
179                        log.debug("new file created");
180                        // add header
181                        fileOut(getHeader(), rs);
182                    }
183                } else {
184                    file = new java.io.File(getFullLoggerFileName(rs));
185                }
186            } catch (IOException e) {
187                log.error("Exception while making logging directory: {}", e.getLocalizedMessage());
188            }
189        }
190        return file;
191    }
192
193    public String getFullLoggerFileName() {
194        return loggingDirectory + File.separator + getFileName();
195    }
196
197    private String operationsDirectory =
198            OperationsSetupXml.getFileLocation() + OperationsSetupXml.getOperationsDirectoryName();
199
200    private String loggingDirectory = operationsDirectory + File.separator + "logger"; // NOI18N
201
202    public String getDirectoryName() {
203        return loggingDirectory;
204    }
205
206    public void setDirectoryName(String name) {
207        loggingDirectory = name;
208    }
209
210    // Use the same common file even if the session crosses midnight
211    private String fileName;
212
213    public String getFileName() {
214        if (fileName == null) {
215            fileName = getDate() + ".csv"; // NOI18N
216        }
217        return fileName;
218    }
219
220    /**
221     * Individual files for each rolling stock stored in a directory called
222     * "rollingStock" inside the "logger" directory.
223     * @param rs The RollingStock to log.
224     * @return Full path name of log file.
225     *
226     */
227    public String getFullLoggerFileName(RollingStock rs) {
228        if (!OperationsXml.checkFileName(rs.toString())) { // NOI18N
229            log.error("Rolling stock name ({}) must not contain reserved characters", rs);
230            return loggingDirectory + File.separator + "rollingStock" + File.separator + "ERROR" + ".csv"; // NOI18N
231        }
232        String rsName = rs.toString();
233        // put clones in the same file as original car
234        if (rs.isClone()) {
235            rsName = rs.getRoadName() + " " + rs.getNumber().split(Car.CLONE_REGEX)[0];
236        }
237        return loggingDirectory + File.separator + "rollingStock" + File.separator + rsName + ".csv"; // NOI18N
238    }
239
240    private String getDate() {
241        LocalDateTime date = LocalDateTime.now();
242        DateTimeFormatter simpleDateFormat = DateTimeFormatter.ofPattern("yyyy_MM_dd"); // NOI18N
243        return date.format(simpleDateFormat);
244    }
245
246    /**
247     * Return the date and time in an MS Excel friendly format yyyy/MM/dd
248     * HH:mm:ss
249     *
250     */
251    private String getTime() {
252        LocalDateTime date = LocalDateTime.now();
253        DateTimeFormatter simpleDateFormat = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); // NOI18N
254        return date.format(simpleDateFormat);
255    }
256
257    private void addCarListeners() {
258        if (Setup.isCarLoggerEnabled() && !carLog) {
259            log.debug("Rolling Stock Logger adding car listerners");
260            carLog = true;
261            List<Car> cars = InstanceManager.getDefault(CarManager.class).getList();
262            cars.forEach(car -> car.addPropertyChangeListener(this));
263            // listen for new rolling stock being added
264            InstanceManager.getDefault(CarManager.class).addPropertyChangeListener(this);
265        }
266    }
267
268    private void addEngineListeners() {
269        if (Setup.isEngineLoggerEnabled() && !engLog) {
270            engLog = true;
271            log.debug("Rolling Stock Logger adding engine listerners");
272            List<Engine> engines = InstanceManager.getDefault(EngineManager.class).getList();
273            engines.forEach(engine -> engine.addPropertyChangeListener(this));
274            // listen for new rolling stock being added
275            InstanceManager.getDefault(EngineManager.class).addPropertyChangeListener(this);
276        }
277    }
278
279    private void removeCarListeners() {
280        if (carLog) {
281            log.debug("Rolling Stock Logger removing car listerners");
282            List<Car> cars = InstanceManager.getDefault(CarManager.class).getList();
283            cars.forEach(car -> car.removePropertyChangeListener(this));
284            InstanceManager.getDefault(CarManager.class).removePropertyChangeListener(this);
285        }
286        carLog = false;
287    }
288
289    private void removeEngineListeners() {
290        if (engLog) {
291            log.debug("Rolling Stock Logger removing engine listerners");
292            List<Engine> engines = InstanceManager.getDefault(EngineManager.class).getList();
293            engines.forEach(engine -> engine.removePropertyChangeListener(this));
294            InstanceManager.getDefault(EngineManager.class).removePropertyChangeListener(this);
295        }
296        engLog = false;
297    }
298
299    public void dispose() {
300        removeCarListeners();
301        removeEngineListeners();
302    }
303
304    @Override
305    public void propertyChange(PropertyChangeEvent e) {
306        if (e.getPropertyName().equals(RollingStock.TRACK_CHANGED_PROPERTY)) {
307            if (Control.SHOW_PROPERTY) {
308                log.debug("Logger sees property change for car {}", e.getSource());
309            }
310            store((RollingStock) e.getSource());
311        }
312        if (e.getPropertyName().equals(RollingStockManager.LISTLENGTH_CHANGED_PROPERTY)) {
313            if ((Integer) e.getNewValue() > (Integer) e.getOldValue()) {
314                // a car or engine has been added
315                if (e.getSource().getClass().equals(CarManager.class)) {
316                    removeCarListeners();
317                    addCarListeners();
318                } else if (e.getSource().getClass().equals(EngineManager.class)) {
319                    removeEngineListeners();
320                    addEngineListeners();
321                }
322            }
323        }
324    }
325
326    private static final Logger log = LoggerFactory.getLogger(RollingStockLogger.class);
327}