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