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.*; 020import jmri.jmrit.operations.setup.backup.AutoBackup; 021import jmri.jmrit.operations.setup.backup.DefaultBackup; 022 023/** 024 * Logs train movements and status to a file. 025 * 026 * @author Daniel Boudreau Copyright (C) 2010, 2013, 2024 027 */ 028public class TrainLogger extends XmlFile implements InstanceManagerAutoDefault, PropertyChangeListener { 029 030 File _fileLogger; 031 private boolean _trainLog = false; // when true logging train movements 032 033 public TrainLogger() { 034 } 035 036 public void enableTrainLogging(boolean enable) { 037 if (enable) { 038 addTrainListeners(); 039 } else { 040 removeTrainListeners(); 041 } 042 } 043 044 private void createFile() { 045 if (!Setup.isTrainLoggerEnabled()) { 046 return; 047 } 048 if (_fileLogger != null) { 049 return; // log file has already been created 050 } // create the logging file for this session 051 try { 052 if (!checkFile(getFullLoggerFileName())) { 053 // The file/directory does not exist, create it before writing 054 _fileLogger = new java.io.File(getFullLoggerFileName()); 055 File parentDir = _fileLogger.getParentFile(); 056 if (!parentDir.exists()) { 057 if (!parentDir.mkdirs()) { 058 log.error("logger directory not created"); 059 } 060 } 061 if (_fileLogger.createNewFile()) { 062 log.debug("new file created"); 063 // add header 064 fileOut(getHeader()); 065 } 066 } else { 067 _fileLogger = new java.io.File(getFullLoggerFileName()); 068 } 069 } catch (Exception e) { 070 log.error("Exception while making logging directory", e); 071 } 072 073 } 074 075 private void store(Train train) { 076 // create train file if needed 077 createFile(); 078 // Note that train status can contain a comma 079 List<Object> line = Arrays.asList(new Object[]{train.getName(), 080 train.getDescription(), 081 train.getCurrentLocationName(), 082 train.getNextLocationName(), 083 train.getNumberCarsInTrain(), 084 train.getNumberCarsPickedUp(), 085 train.getNumberCarsSetout(), 086 train.getTrainLength(), 087 train.getTrainWeight(), 088 train.getStatus(), 089 train.getBuildFailedMessage(), 090 getDateAndTime()}); 091 fileOut(line); 092 } 093 094 ResourceBundle rb = ResourceBundle 095 .getBundle("jmri.jmrit.operations.setup.JmritOperationsSetupBundle"); 096 097 /* 098 * Adds a status line to the log file whenever the trains file is saved. 099 */ 100 private void storeFileSaved() { 101 List<Object> line = Arrays.asList(new Object[]{ 102 Bundle.getMessage("TrainLogger"), // train name 103 "", // train description 104 "", // current location 105 "", // next location name 106 "", // cars 107 "", // pulls 108 "", // drops 109 "", // length 110 "", // weight 111 Setup.isAutoSaveEnabled() ? rb.getString("AutoSave") : Bundle.getMessage("Manual"), // status 112 Bundle.getMessage("TrainsSaved"), // build messages 113 getDateAndTime()}); 114 fileOut(line); 115 } 116 117 private void storeBackupChanged(PropertyChangeEvent e) { 118 // create train file if needed 119 createFile(); 120 List<Object> line = Arrays.asList(new Object[]{ 121 Bundle.getMessage("TrainLogger"), // train name 122 "", // train description 123 "", // current location 124 "", // next location name 125 "", // cars 126 "", // pulls 127 "", // drops 128 "", // length 129 e.getPropertyName(), // weight 130 e.getOldValue(), // status 131 e.getNewValue(), // build messages 132 getDateAndTime()}); 133 fileOut(line); 134 } 135 136 private List<Object> getHeader() { 137 return Arrays.asList(new Object[]{Bundle.getMessage("Name"), 138 Bundle.getMessage("Description"), 139 Bundle.getMessage("Current"), 140 Bundle.getMessage("NextLocation"), 141 Bundle.getMessage("Cars"), 142 Bundle.getMessage("Pulls"), 143 Bundle.getMessage("Drops"), 144 Bundle.getMessage("Length"), 145 Bundle.getMessage("Weight"), 146 Bundle.getMessage("Status"), 147 Bundle.getMessage("BuildMessages"), 148 Bundle.getMessage("DateAndTime")}); 149 } 150 151 /* 152 * Appends one line to file. 153 */ 154 private void fileOut(List<Object> line) { 155 if (_fileLogger == null) { 156 log.error("Log file doesn't exist"); 157 return; 158 } 159 160 // FileOutputStream is set to append 161 try (CSVPrinter fileOut = new CSVPrinter(new BufferedWriter(new OutputStreamWriter( 162 new FileOutputStream(_fileLogger, true), StandardCharsets.UTF_8)), CSVFormat.DEFAULT)) { 163 log.debug("Log: {}", line); 164 fileOut.printRecord(line); 165 fileOut.flush(); 166 fileOut.close(); 167 } catch (IOException e) { 168 log.error("Exception while opening log file: {}", e.getLocalizedMessage()); 169 } 170 } 171 172 private void addTrainListeners() { 173 if (Setup.isTrainLoggerEnabled() && !_trainLog) { 174 log.debug("Train Logger adding train listerners"); 175 _trainLog = true; 176 List<Train> trains = InstanceManager.getDefault(TrainManager.class).getTrainsByIdList(); 177 trains.forEach(train -> train.addPropertyChangeListener(this)); 178 // listen for new trains being added 179 InstanceManager.getDefault(TrainManager.class).addPropertyChangeListener(this); 180 // listen for backup file changes 181 InstanceManager.getDefault(DefaultBackup.class).addPropertyChangeListener(this); 182 InstanceManager.getDefault(AutoBackup.class).addPropertyChangeListener(this); 183 } 184 } 185 186 private void removeTrainListeners() { 187 log.debug("Train Logger removing train listerners"); 188 if (_trainLog) { 189 List<Train> trains = InstanceManager.getDefault(TrainManager.class).getTrainsByIdList(); 190 trains.forEach(train -> train.removePropertyChangeListener(this)); 191 InstanceManager.getDefault(TrainManager.class).removePropertyChangeListener(this); 192 InstanceManager.getDefault(DefaultBackup.class).removePropertyChangeListener(this); 193 InstanceManager.getDefault(AutoBackup.class).removePropertyChangeListener(this); 194 } 195 _trainLog = false; 196 } 197 198 public void dispose() { 199 removeTrainListeners(); 200 } 201 202 @Override 203 public void propertyChange(PropertyChangeEvent e) { 204 if (e.getPropertyName().equals(Train.TRAIN_CURRENT_CHANGED_PROPERTY) && e.getNewValue() != null || 205 e.getPropertyName().equals(Train.STATUS_CHANGED_PROPERTY) && 206 (e.getNewValue().equals(Train.TRAIN_RESET) || 207 e.getNewValue().equals(Train.BUILDING) || 208 e.getNewValue().equals(Train.BUILD_FAILED) || 209 e.getNewValue().toString().startsWith(Train.TERMINATED))) { 210 if (Control.SHOW_PROPERTY) { 211 log.debug("Train logger sees property change for train ({}), property name: {}", e.getSource(), 212 e.getPropertyName()); 213 } 214 store((Train) e.getSource()); 215 } 216 if (e.getPropertyName().equals(TrainManager.LISTLENGTH_CHANGED_PROPERTY)) { 217 if ((Integer) e.getNewValue() > (Integer) e.getOldValue()) { 218 // a car or engine has been added 219 removeTrainListeners(); 220 addTrainListeners(); 221 } 222 } 223 if (e.getPropertyName().equals(TrainManager.TRAINS_SAVED_PROPERTY)) { 224 storeFileSaved(); 225 } 226 if (e.getPropertyName().equals(DefaultBackup.COPY_FILES_CHANGED_PROPERTY)) { 227 storeBackupChanged(e); 228 } 229 } 230 231 public String getFullLoggerFileName() { 232 return loggingDirectory + File.separator + getFileName(); 233 } 234 235 private String operationsDirectory = 236 OperationsSetupXml.getFileLocation() + OperationsSetupXml.getOperationsDirectoryName(); 237 private String loggingDirectory = operationsDirectory + File.separator + "logger" + File.separator + "trains"; // NOI18N 238 239 public String getDirectoryName() { 240 return loggingDirectory; 241 } 242 243 public void setDirectoryName(String name) { 244 loggingDirectory = name; 245 } 246 247 private String fileName; 248 249 public String getFileName() { 250 if (fileName == null) { 251 fileName = Bundle.getMessage("Trains") + "_" + getDate() + ".csv"; // NOI18N 252 } 253 return fileName; 254 } 255 256 private String getDate() { 257 Date date = Calendar.getInstance().getTime(); 258 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy_MM_dd"); // NOI18N 259 return simpleDateFormat.format(date); 260 } 261 262 /** 263 * Return the date and time in an MS Excel friendly format yyyy/MM/dd 264 * HH:mm:ss 265 */ 266 private String getDateAndTime() { 267 String time = Calendar.getInstance().getTime().toString(); 268 SimpleDateFormat dt = new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy"); // NOI18N 269 SimpleDateFormat dtout = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); // NOI18N 270 try { 271 return dtout.format(dt.parse(time)); 272 } catch (ParseException e) { 273 return time; // there was an issue, use the old format 274 } 275 } 276 277 private final static Logger log = LoggerFactory.getLogger(TrainLogger.class); 278}