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 (rs.getClass().equals(Car.class)) {
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            fileOut.flush();
111            fileOut.close();
112        } catch (IOException e) {
113            log.error("Exception while opening log file: {}", e.getLocalizedMessage());
114        }
115    }
116
117    /*
118     * Returns the common log file for all rolling stock
119     */
120    private File getFile() {
121        File fileLogger = null;
122        if (Setup.isEngineLoggerEnabled() || Setup.isCarLoggerEnabled()) {
123            try {
124                if (!checkFile(getFullLoggerFileName())) {
125                    // The file/directory does not exist, create it before writing
126                    fileLogger = new java.io.File(getFullLoggerFileName());
127                    File parentDir = fileLogger.getParentFile();
128                    if (!parentDir.exists()) {
129                        if (!parentDir.mkdirs()) {
130                            log.error("logger directory not created");
131                        }
132                    }
133                    if (fileLogger.createNewFile()) {
134                        log.debug("new file created");
135                        // add header
136                        fileOut(getHeader());
137                    }
138                } else {
139                    fileLogger = new java.io.File(getFullLoggerFileName());
140                }
141            } catch (IOException e) {
142                log.error("Exception while making logging directory: {}", e.getLocalizedMessage());
143            }
144        }
145        return fileLogger;
146    }
147
148    private List<Object> getHeader() {
149        return Arrays.asList(new Object[]{Bundle.getMessage("Number"),
150            Bundle.getMessage("Road"),
151            Bundle.getMessage("Type"),
152            Bundle.getMessage("Load"),
153            Bundle.getMessage("Location"),
154            Bundle.getMessage("Track"),
155            Bundle.getMessage("FinalDestination"),
156            Bundle.getMessage("Track"),
157            Bundle.getMessage("Train"),
158            Bundle.getMessage("Moves"),
159            Bundle.getMessage("DateAndTime")});
160    }
161
162    /*
163     * Gets the individual log file for a specific car or loco.
164     */
165    private File getFile(RollingStock rs) {
166        File file = null;
167        if (Setup.isEngineLoggerEnabled() || Setup.isCarLoggerEnabled()) {
168            // create the logging file for this rolling stock
169            try {
170                if (!checkFile(getFullLoggerFileName(rs))) {
171                    // The file/directory does not exist, create it before writing
172                    file = new java.io.File(getFullLoggerFileName(rs));
173                    File parentDir = file.getParentFile();
174                    if (!parentDir.exists()) {
175                        if (!parentDir.mkdirs()) {
176                            log.error("logger directory not created");
177                        }
178                    }
179                    if (file.createNewFile()) {
180                        log.debug("new file created");
181                        // add header
182                        fileOut(getHeader(), rs);
183                    }
184                } else {
185                    file = new java.io.File(getFullLoggerFileName(rs));
186                }
187            } catch (IOException e) {
188                log.error("Exception while making logging directory: {}", e.getLocalizedMessage());
189            }
190        }
191        return file;
192    }
193
194    public String getFullLoggerFileName() {
195        return loggingDirectory + File.separator + getFileName();
196    }
197
198    private String operationsDirectory =
199            OperationsSetupXml.getFileLocation() + OperationsSetupXml.getOperationsDirectoryName();
200
201    private String loggingDirectory = operationsDirectory + File.separator + "logger"; // NOI18N
202
203    public String getDirectoryName() {
204        return loggingDirectory;
205    }
206
207    public void setDirectoryName(String name) {
208        loggingDirectory = name;
209    }
210
211    // Use the same common file even if the session crosses midnight
212    private String fileName;
213
214    public String getFileName() {
215        if (fileName == null) {
216            fileName = getDate() + ".csv"; // NOI18N
217        }
218        return fileName;
219    }
220
221    /**
222     * Individual files for each rolling stock stored in a directory called
223     * "rollingStock" inside the "logger" directory.
224     * @param rs The RollingStock to log.
225     * @return Full path name of log file.
226     *
227     */
228    public String getFullLoggerFileName(RollingStock rs) {
229        if (!OperationsXml.checkFileName(rs.toString())) { // NOI18N
230            log.error("Rolling stock name ({}) must not contain reserved characters", rs);
231            return loggingDirectory + File.separator + "rollingStock" + File.separator + "ERROR" + ".csv"; // NOI18N
232        }
233        return loggingDirectory + File.separator + "rollingStock" + File.separator + rs.toString() + ".csv"; // NOI18N
234    }
235
236    private String getDate() {
237        Date date = Calendar.getInstance().getTime();
238        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy_MM_dd"); // NOI18N
239        return simpleDateFormat.format(date);
240    }
241
242    /**
243     * Return the date and time in an MS Excel friendly format yyyy/MM/dd
244     * HH:mm:ss
245     *
246     */
247    private String getTime() {
248        String time = Calendar.getInstance().getTime().toString();
249        SimpleDateFormat dt = new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy"); // NOI18N
250        SimpleDateFormat dtout = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); // NOI18N
251        try {
252            return dtout.format(dt.parse(time));
253        } catch (ParseException e) {
254            return time; // there was an issue, use the old format
255        }
256    }
257
258    private void addCarListeners() {
259        if (Setup.isCarLoggerEnabled() && !carLog) {
260            log.debug("Rolling Stock Logger adding car listerners");
261            carLog = true;
262            List<Car> cars = InstanceManager.getDefault(CarManager.class).getList();
263            cars.forEach(car -> car.addPropertyChangeListener(this));
264            // listen for new rolling stock being added
265            InstanceManager.getDefault(CarManager.class).addPropertyChangeListener(this);
266        }
267    }
268
269    private void addEngineListeners() {
270        if (Setup.isEngineLoggerEnabled() && !engLog) {
271            engLog = true;
272            log.debug("Rolling Stock Logger adding engine listerners");
273            List<Engine> engines = InstanceManager.getDefault(EngineManager.class).getList();
274            engines.forEach(engine -> engine.addPropertyChangeListener(this));
275            // listen for new rolling stock being added
276            InstanceManager.getDefault(EngineManager.class).addPropertyChangeListener(this);
277        }
278    }
279
280    private void removeCarListeners() {
281        if (carLog) {
282            log.debug("Rolling Stock Logger removing car listerners");
283            List<Car> cars = InstanceManager.getDefault(CarManager.class).getList();
284            cars.forEach(car -> car.removePropertyChangeListener(this));
285            InstanceManager.getDefault(CarManager.class).removePropertyChangeListener(this);
286        }
287        carLog = false;
288    }
289
290    private void removeEngineListeners() {
291        if (engLog) {
292            log.debug("Rolling Stock Logger removing engine listerners");
293            List<Engine> engines = InstanceManager.getDefault(EngineManager.class).getList();
294            engines.forEach(engine -> engine.removePropertyChangeListener(this));
295            InstanceManager.getDefault(EngineManager.class).removePropertyChangeListener(this);
296        }
297        engLog = false;
298    }
299
300    public void dispose() {
301        removeCarListeners();
302        removeEngineListeners();
303    }
304
305    @Override
306    public void propertyChange(PropertyChangeEvent e) {
307        if (e.getPropertyName().equals(RollingStock.TRACK_CHANGED_PROPERTY)) {
308            if (Control.SHOW_PROPERTY) {
309                log.debug("Logger sees property change for car {}", e.getSource());
310            }
311            store((RollingStock) e.getSource());
312        }
313        if (e.getPropertyName().equals(RollingStockManager.LISTLENGTH_CHANGED_PROPERTY)) {
314            if ((Integer) e.getNewValue() > (Integer) e.getOldValue()) {
315                // a car or engine has been added
316                if (e.getSource().getClass().equals(CarManager.class)) {
317                    removeCarListeners();
318                    addCarListeners();
319                } else if (e.getSource().getClass().equals(EngineManager.class)) {
320                    removeEngineListeners();
321                    addEngineListeners();
322                }
323            }
324        }
325    }
326
327    private final static Logger log = LoggerFactory.getLogger(RollingStockLogger.class);
328}