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}